深入理解Linux网络技术内幕——IPv4 报文的传输发送

报文传输,指的是报文离开本机,发往其他系统的过程。

传输可以由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把数据报传递给设备驱动程序。



























郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。