深入理解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把数据报传递给设备驱动程序。

时间: 2024-08-01 06:31:38

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

深入理解Linux网络技术内幕——IPv4 报文的接收(转发与本地传递)

我们知道,报文经过网卡驱动处理后,调用net_receive_skb传递给具体的协议处理函数,对于IPv4报文来说,其协议处理函数就是ip_rcv了,ip_rcv在进行一些健康检查等操作后,会调用ip_rcv_finish来处理报文.这也是IPv4协议对报文接收处理的开始. 我们先看下ip_rcv_finish源代码: ip_rcv_finish: //ip数据报文的主要处理程序(ip_rcv仅仅只是对ip数据报做一些健康性检查) //ip_rcv_finish 其实是进行路由表查询,,决定报文

深入理解Linux网络技术内幕——IPv4 分段与重组

封包的分段和重组是IP协议最重要的工作之一. IPv4报头中有一个len字段(用于表示报文的总长度,单位:字节)占16bit,因此,封包的最大尺寸定义为64K,(2^16/1024=64). 但是,在实际网络传输中,没有几个网络接口能够传输64K这么大的封包,而是有一个MTU表示其最大传输单元.这样,当要传输的封包大于MTU时,就需要对封包进行分段. 这里需要说明,我们指的MTU,不仅仅是出口设备的MTU,它取决于很多因素,如路由表项所用的MTU.出口设备的MTU等. 我们先不用过多与关注MTU

深入理解Linux网络技术内幕——IPv4 概念

1.大蓝图 大蓝图展示了IPv4协议与其他子系统之间的联系,保罗设备驱动.Netfilter.L4 层协议等之间的互动. IPv4协议中的报文 我们可以大致看出数据在IPv4协议中的流向, 接收报文 设备驱动处理完硬件介绍到的数据后,IPv4协议的ip_rcv函数(net_receive_skb调用)得到了属于IPv4的报文,接着调用ip_rcv_finish对报文进行分析.判断是该转发还是交付本地上层协议. 如果是本地报文,则传给ip_local_deliver处理,如果是转发,那就交付ip_

《深入理解Linux网络技术内幕》阅读笔记 --- 路由

一.Linux内核中路由相关的主要数据结构 struct fib_result:对路由表查找后返回该结构,它的内容并不是简单的包含下一跳信息,而且包含其他特性,例如策略路由所需的更多参数. struct fib_rule:表示由策略路由在路由流量时选择路由表的规则 struct fib_node:一条路由表项.例如,该数据结构用于存储由route add或ip route add命令添加一条路由时生成的信息. struct fn_zone:一个zone表示子网掩码长度相同的一组路由 struct

深入理解Linux网络技术内幕——协议处理函数

网络帧在进入网络层时,需要区分不同的网络协议进行处理,这就需要涉及协议处理函数. 首先我们从驱动接收到一个数据帧,分析数据帧在协议栈中自下而上的传输流程. 设备驱动程序在接收到一个数据帧时,会将其保存在一个sk_buff缓冲区数据结构,并对其进行初始化. struct sk_buff { ...... __be16 protocol:16; ...... } 在这个缓冲区结构体中,有一个protocol字段,用于标识网络层的协议. 我们知道网络帧在设备驱动程序中处理后,设备驱动程序会调用neti

深入理解Linux网络技术内幕——路由子系统的概念与高级路由

本文讨论IPv4的路由子系统.(IPv6对路由的处理不同). 基本概念 路由子系统工作在三层,用来转发入口流量. 路由子系统主要设计 路由器.路由.路由表等概念. 路由器: 配备多个网络接口卡(NIC),并且能利用自身网络信息进行入口流量转发的设备. 路由: 流量转发,决定目的地的过程 路由表:转发信息库,该库中储存路由需要本地接收还是转发的信息, 以及转发流量时所需要的信息.(即,信息库用来判断,要不要转发,如果要转发,向哪里转发). 我们了解,路由器有多个网卡,但是多个NIC的设备不一定就是

深入理解Linux网络技术内幕——设备的注册于初始化(一)

副标题:设备注册相关的基本结构的原理框架 设备注册与删除时间 设备在下列两种情况下进行注册: 1)加载NIC驱动时 2)插入热插拔设备时 这里NIC与热插拔设备有些不同.a.对于非热插拔NIC来说,NIC的注册是伴随着其驱动的发生的,而NIC可以内建到内核,也可以作为模块载入,如果内建入内核,则NIC设备和初始化均发生在引导时,如果NIC作为模块加载,则NIC的注册和驱动初始化均发生在模块加载时.b. 对于热插拔NIC设备来说,其驱动已经加载,因此设备的注册发生在插入设备,内核通知关联驱动时.

深入理解Linux网络技术内幕——帧的接收与传输

帧的接收 NAPI与netif_rx(非NAPI) Linux内核获取网络帧到达通知的方式有两中:中断和轮询.(中断值设备向内核发出中断,轮询指linux内核主动轮询设备) 在早起的linux内核中,网络帧主要以中断的方式通知linux内核帧的到达.这是非NAPI方式. 现在的操作系统中,linux内核使用NAPI方式, 获取帧到达的消息.NAPI混合使用了中断和轮询. netif_rx(非NAPI): 每一个帧接收完毕时,设备向内核发送一个中断.(在低流量负载的情况下,这种方式对比轮询优势明显

深入理解Linux网络技术内幕——网络设备初始化

概述 内核的初始化过程过程中,与网络相关的工作如下所示: 内核引导时执行start_kernel,start_kernel结束之前会调用rest_init,rest_init初始化内核线程init(在Linux3-12中为kernel_init). asmlinkage void __init start_kernel(void) { ... parse_early_param();//间接调用parse_args parse_args(...); //处理内核引导程序(boot loader)