报文传输,指的是报文离开本机,发往其他系统的过程。
传输可以由L4层协议发起,也可以由报文转发发起。
在深入理解Linux网络技术内幕——IPv4 报文的接收(转发与本地传递)一文中,我们可以看到,报文转发最后会调用dst_output与邻居子系统进行交互,然后传给设备驱动程序。 这里,我们从L4层协议发起的传输,最后也会经历这一过程(调用dst_output)。本文讨论的是L4层协议发起的传输,在IPv4协议处理(IP层)中的一些环节。
大蓝图
我们先看下传输环节的大蓝图,以便对传输这一过程有大概的过程。
我们看到L4层协议(如TCP、UDP),以及一些特殊的三层协议(ICMP,RAW IP等)最终都会调用dst_output来将报文传给驱动程序。
调用dst_output之前的处理可以分为图中四种情形(不考虑报文转发发起的传输)。
case1a 和case1b主要针对(UDP、ICMP、RAWIP),分别调用ip_append_data和ip_append_page(其实是ip_append_data的变种),来将报文保存在缓冲区中(先不传输),待到缓冲区需要刷新时,才通过ip_push_pending_frames末尾间接调用dst_output来完成传输工作。
case2 面对TCP和SCTP,会直接调用ip_queue_xmit处理报文,然后调用dst_output。
case3 针对RAWIP和IGMP,直接调用dst_output。
上面的分类知识针对一般情况,也有一些特殊情形,比如TCP在需要发送ACK和RESET报文,会使用ip_send_reply,并间接调用ip_append_data和ip_push_pending_frames。TCP在传输ACK SYN时,也会调用ip_build_and_send_pkt。
传输环节-内核的主要任务
1.查询下一跳点
——涉及路由子系统
2.初始化IP报头
——填写一些字段
3.处理选项
——设置一些需要的选项(博主其它博文会进行介绍)
4.分段
——IP包太大时,传输前必须分段
5.检验和
——
6.Netfilter检查
——
7.更新统计数据
——
ip_queue_xmit情形
ip_queue_xmit是TCP和SCTP所使用的函数。
//由tcp、sctp使用
//skb:封包描述符
//ipfragok: sctp使用的标志,指明是否可以分段
int ip_queue_xmit(struct sk_buff *skb, int ipfragok)
{
struct sock *sk = skb->sk;
struct inet_sock *inet = inet_sk(sk); //要通过的套接字
struct ip_options_rcu *inet_opt = NULL;
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();
rt = skb_rtable(skb); //如果缓冲区已经设置了正确的路由信息,就不需要查找路由表了
if (rt != NULL)
goto packet_routed;
/* Make sure we can route this packet. */
rt = (struct rtable *)__sk_dst_check(sk, 0);
inet_opt = rcu_dereference(inet->inet_opt); //选项初始化
if (rt == NULL) {
__be32 daddr;
/* Use correct destination address if we have options. */
daddr = inet->daddr;
if (inet_opt && inet_opt->opt.srr)
daddr = inet_opt->opt.faddr;
{
struct flowi fl = { .oif = sk->sk_bound_dev_if,
.mark = sk->sk_mark,
.nl_u = { .ip4_u =
{ .daddr = daddr,
.saddr = inet->saddr,
.tos = RT_CONN_FLAGS(sk) } },
.proto = sk->sk_protocol,
.flags = inet_sk_flowi_flags(sk),
.uli_u = { .ports =
{ .sport = inet->sport,
.dport = inet->dport } } };
/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times
* itself out.
*/
security_sk_classify_flow(sk, &fl);
if (ip_route_output_flow(sock_net(sk), &rt, &fl, sk, 0))
goto no_route;
}
sk_setup_caps(sk, &rt->u.dst);
}
skb_dst_set(skb, dst_clone(&rt->u.dst));
packet_routed:
if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_dst != rt->rt_gateway)
goto no_route;
/* OK, we know where to send it, allocate and build IP header. */
//把skb-》data往回移动,使其指向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->u.dst) && !ipfragok)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
iph->ttl = ip_select_ttl(inet, &rt->u.dst);
iph->protocol = sk->sk_protocol;
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
/* Transport layer set skb->h.foo itself. */
if (inet_opt && inet_opt->opt.optlen) {
iph->ihl += inet_opt->opt.optlen >> 2;
ip_options_build(skb, &inet_opt->opt, inet->daddr, rt, 0);
}
ip_select_ident_more(iph, &rt->u.dst, sk,
(skb_shinfo(skb)->gso_segs ?: 1) - 1);
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
res = ip_local_out(skb);
rcu_read_unlock();
return res;
no_route:
rcu_read_unlock();
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
kfree_skb(skb);
return -EHOSTUNREACH;
}
ip_push_pending_frames的情形
在前面的大蓝图case1a和case1b中,我们看到,一些L4层的协议会把数据通过ip_append_data或ip_append_page把数据线放在缓冲区,然后再显示调用ip_push_pending_frames传送数据。
把数据放在缓冲区有两个优点,一方面,缓冲区的数据可以被后续的一些函数使用,构成一些片段;另一方面,把数据放缓冲区,等缓冲区满了(达到PMTU)再传送数据,可以更有效率。
如果在一些情况下,L4层希望去放在缓冲区的数据立即被传输,那么在调用ip_append_data把数据放缓冲区后,立即调用ip_push_pending_frames进行传输。
ip_append_data
ip_append_data主要有以下几项任务:
1. 组织缓冲区。把L4层的报文数据组织到缓冲区,使这些缓冲区能够更好的处理分段。也能让L2、L3在稍后能够更容易添加报头。
2. 优化内存分配 。这里要考虑到上层协议信息,以及设备出口的传输能力。
3. 处理L4检验和。
ip_append_data 这部分的内容还没完全搞明白,最近没时间细看了,以后有空了再来更新,先Mark下。
IPv4报文的传输最后调用dst_output,然后简介调用ip_finish_output2与邻居子系统进行交互。最终调用dev_queue_xmit把数据报传递给设备驱动程序。