关于tcp-ip:Linux-IP协议源码分析

96次阅读

共计 7766 个字符,预计需要花费 20 分钟才能阅读完成。

IP 协定 是网络的最重要局部,毫不夸大地说,正是因为有 IP 协定 才有了互联网。而 IP 协定 最重要的是 IP 地址IP 地址 就如同咱们的家庭住址一样,用于其他人不便找到咱们的地位。

当然,这篇文章并不是介绍 IP 协定 的原理,无关 IP 协定 的原理能够参考经典的书籍《TCP/IP 协定详解》,而这篇文章次要介绍的是 Linux 内核怎么实现 IP 协定

IP 协定简介

尽管咱们不会对 IP 协定 做具体介绍,然而还是作个简略的介绍吧,不然间接剖析代码有点唐突。

如果有一台计算机 A 和一台计算机 B,计算机 A 想与计算机 B 通信的话怎么办?

如果计算机 A 与计算机 B 是直连的,那么计算机 A 能够间接发送信息给计算机 B,如下图:

所以,如果是直连的话,问题很容易解决。然而,大多数状况下,互联网上的计算机都不是直连的。试想一下,如果在中国的电脑如果要与美国的电脑发送音讯,那是不可能间接通过一条网线连贯的。

那么,互联网上的计算机之间是通过什么连贯的的?答案就是 路由器。如下图:

因为在互联网中,计算机与计算机之间不是直连的,所以两台计算机之间要通信的话不能间接发送音讯,因为不同的计算机之间不晓得对方的地位。

那么,有什么方法解决呢?咱们现实生活中,房子都有一个固定的地址,如:广东省广州市天河区林和西路 98 号,咱们能够通过这个地址找到对应的房子。所以,对应互联网上的计算机,咱们也能够人为的为其编上地址,名为 IP 地址

IP 地址 由一个 32 位的整型数字示意,学过计算机科学的同学都指定,一个 32 位的整型数字可能示意的范畴为:0 ~ 4294967295。所以,IP 地址 实践上可能反对 4294967296 台计算机上网(但事实上远远少于这个数,因为有很多地址用于非凡用处)。

然而,32 位的整型数字对人类的记忆不太敌对,所以,又将这个 32 位的整型数字分成 4 个 8 位的整型数字,而后用 将他们连接起来,如下图:

所以,IP 地址 示意的范畴如下:

0.0.0.0 ~ 255.255.255.255

咱们能够在 Windows 零碎的网络设置处查看到本机的 IP 地址,如下图:

有了 IP 地址 后,就能够为互联网上的每台计算机设置 IP 地址,如下图:

这样,为每台计算机设置好 IP 地址 后,不同计算机之间就能够通过 IP 地址 来进行通信。比方,计算机 A 想与计算机 D 通信,那么就能够通过向 IP 地址11.11.1.1 的地址发送音讯。

通信过程如下:

  • 计算 A 把本人的 IP 地址(源 IP 地址) 与计算机 B 的 IP 地址(指标 IP 地址) 封装成数据包,而后发送到路由器 A。
  • 路由器 A 接管到计算机 A 的数据包,发现指标 IP 地址 不在同一个网段(也就是连贯不同路由器的),就会从 路由表 中找到适合的下一跳 路由器,把数据包转发进来(也就是路由 B)。
  • 路由器 B 接管到路由器 A 的数据后,从数据包中获取到指标 IP 地址11.11.1.1,晓得此 IP 地址 是计算机 D 的 IP 地址,所以就把数据转发给计算机 D。

因为每个路由器都晓得连着本人的所有计算机的 IP 地址 (称为路由表),所以路由器与路由器之间能够通过 路由协定 替换路由表的信息,这样就能够从路由表信息中查找到 IP 地址 对应的下一跳路由器。

IP 协定 就介绍到这里了,更具体的原理能够参考其余材料。

IP 头部

因为向网络中的计算机发送数据时,必须指定对方的 IP 地址(指标 IP 地址) 和本机的 IP 地址(源 IP 地址),所以须要在发送的数据包增加 IP 协定 头部。IP 协定 头部的格局如下图所示:

从上图能够看出,除了 指标 IP 地址 源 IP 地址 外,还有其余一些字段,这些字段都是为了实现 IP 协定 而定义的。上面咱们来介绍一下 IP 头部 各个字段的作用:

  • 版本:占 4 个位。示意 IP 协定 的版本,如果是 IPv4 的话,那么固定为 4。
  • 头部长度:占 4 个位。示意 IP 头部 的长度,单位为字(即 4 个字节)。因为其最大值为 15,所以 IP 头部 最长为 60 字节(15 * 4)。
  • 服务类型:占 8 个位。定义不同的服务类型,能够为 IP 数据包提供不同的服务。本文不波及这个字段,所以不作具体介绍。
  • 总长度:占 16 个位。示意整个 IP 数据包的长度,包含数据与 IP 头部,所以 IP 数据包的最大长度为 65535 字节。
  • ID:占 16 个位。用于标识不同的 IP 数据包。该字段和 Flags分片偏移量 字段配合应用,对较大的 IP 数据包进行分片操作,对于 IP 数据包分片性能前面会介绍。
  • 标记(Flags):占 3 个位。该字段第一位不应用,第二位是 DF(Don't Fragment) 位,DF 位设为 1 时表明路由器不能对该数据包分段。第三位是 MF(More Fragments) 位,示意以后分片是否为 IP 数据包的最初一个分片,如果是最初一个分片,就设置为 0,否则设置为 1。
  • 分片偏移量:占 13 个位。示意以后分片位于 IP 数据包分片组中的地位,接收端靠此来组装还原 IP 数据包。
  • 生存期(TTL):占 8 个位。当 IP 数据包进行发送时,先会对该字段赋予某个特定的值。当 IP 数据包通过沿途每一个路由器时,每个沿途的路由器会将该 IP 数据包的 TTL 值缩小 1。如果 TTL 缩小至为 0 时,则该 IP 数据包会被抛弃。这个字段能够避免因为路由环路而导致 IP 数据包在网络中不停被转发。
  • 下层协定:占 8 个位。标识了下层所应用的协定,例如罕用的 TCP,UDP 等。
  • 校验和:占 16 个位。用于对 IP 头部的正确性进行检测,但不蕴含数据局部。因为每个路由器要扭转 TTL 的值,所以路由器会为每个通过的 IP 数据包从新计算这个值。
  • 源 IP 地址与指标 IP 地址 :这两个字段都占 32 个位。标识了这个 IP 数据包的 源 IP 地址 指标 IP 地址
  • IP 选项:长度可变,最多蕴含 40 字节。选项字段很少被应用,所以本文不会介绍。

IP 头部 构造在内核中的定义如下:

struct iphdr {
    __u8    version:4,
            ihl:4;
    __u8    tos;
    __u16   tot_len;
    __u16   id;
    __u16   frag_off;
    __u8    ttl;
    __u8    protocol;
    __u16   check;
    __u32   saddr;
    __u32   daddr;
    /*The options start here. */
};

IP 头部 构造的各个字段与上图的所展现的字段是一一对应的。

尽管 IP 头部 看起来如同很简单,但如果按每个字段所反对的性能来剖析,就会恍然大悟。一个被增加上 IP 头部 的数据包如下图所示:

当然,除了 IP 头部 外,在一个网络数据包中还可能蕴含一些其余协定的头部,比方 TCP 头部 以太网头部 等,但因为这里只剖析 IP 协定,所以只标出了 IP 头部

接下来,咱们通过源码来剖析 Linux 内核是怎么实现 IP 协定 的,咱们次要剖析 IP 数据包的发送与接管过程。

IP 数据包的发送

要发送一个 IP 数据包,能够通过两个接口来实现:ip_queue_xmit()ip_build_xmit()。第一个次要用于 TCP 协定,而第二个次要用于 UDP 协定。

咱们次要剖析 ip_queue_xmit() 这个接口,ip_queue_xmit() 代码如下:

int ip_queue_xmit(struct sk_buff *skb)
{
    struct sock *sk = skb->sk;
    struct ip_options *opt = sk->protinfo.af_inet.opt;
    struct rtable *rt;
    struct iphdr *iph;

    rt = (struct rtable *)__sk_dst_check(sk, 0); // 是否有路由信息缓存
    if (rt == NULL) {
        u32 daddr;
        u32 tos = RT_TOS(sk->protinfo.af_inet.tos)|RTO_CONN|sk->localroute;

        daddr = sk->daddr;
        if(opt && opt->srr)
            daddr = opt->faddr;

        // 通过指标 IP 地址获取路由信息
        if (ip_route_output(&rt, daddr, sk->saddr, tos, sk->bound_dev_if))
            goto no_route;
        __sk_dst_set(sk, &rt->u.dst); // 设置路由信息缓存
    }

    skb->dst = dst_clone(&rt->u.dst); // 绑定数据包的路由信息
    ...
    // 获取数据包的 IP 头部指针
    iph = (struct iphdr *)skb_push(skb, sizeof(struct iphdr)+(opt?opt->optlen:0));

    // 设置 版本 + 头部长度 + 服务类型
    *((__u16 *)iph) = htons((4<<12)|(5<<8)|(sk->protinfo.af_inet.tos & 0xff));

    iph->tot_len  = htons(skb->len);           // 设置总长度
    iph->frag_off = 0;                         // 分片偏移量
    iph->ttl      = sk->protinfo.af_inet.ttl;  // 生命周期
    iph->protocol = sk->protocol;              // 下层协定(如 TCP 或者 UDP 等)
    iph->saddr    = rt->rt_src;                // 源 IP 地址
    iph->daddr    = rt->rt_dst;                // 指标 IP 地址

    skb->nh.iph = iph;
    ...
    // 调用 ip_queue_xmit2() 进行下一步的发送操作
    return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,
                   ip_queue_xmit2);
}

ip_queue_xmit() 函数的参数是要发送的数据包,其类型为 sk_buff。在内核协定栈中,所有要发送的数据都是通过 sk_buff 构造来作为载体的。ip_queue_xmit() 函数次要实现以下几个工作:

  • 首先调用 __sk_dst_check() 函数获取路由信息缓存,如果路由信息还没被缓存,那么以 指标 IP 地址 作为参数调用 ip_route_output() 函数来获取路由信息,并且设置路由信息缓存。路由信息个别蕴含发送数据的设施对象(网卡设施)和下一跳路由的 IP 地址
  • 绑定数据包的路由信息。
  • 获取数据包的 IP 头部 指针,而后设置 IP 头部 的各个字段的值,如代码正文所示,能够对照 IP 头部 结构图来剖析。
  • 调用 ip_queue_xmit2() 进行下一步的发送操作。

咱们接着剖析 ip_queue_xmit2() 函数的实现,代码如下:

static inline int ip_queue_xmit2(struct sk_buff *skb)
{
    struct sock *sk = skb->sk;
    struct rtable *rt = (struct rtable *)skb->dst;
    struct net_device *dev;
    struct iphdr *iph = skb->nh.iph;
    ...
    // 如果数据包的长度大于设施的最大传输单元, 那么进行分片操作
    if (skb->len > rt->u.dst.pmtu)
        goto fragment;

    if (ip_dont_fragment(sk, &rt->u.dst))         // 如果数据包不能分片
        iph->frag_off |= __constant_htons(IP_DF); // 设置 DF 标记位为 1

    ip_select_ident(iph, &rt->u.dst); // 设置 IP 数据包的 ID(标识符)

    // 计算 IP 头部 的校验和
    ip_send_check(iph);

    skb->priority = sk->priority;
    return skb->dst->output(skb); // 把数据发送进来(个别为 dev_queue_xmit)

fragment:
    ...
    ip_select_ident(iph, &rt->u.dst);
    return ip_fragment(skb, skb->dst->output); // 进行分片操作
}

ip_queue_xmit2() 函数次要实现以下几个工作:

  • 判断数据包的长度是否大于最大传输单元(最大传输单元 Maximum Transmission Unit,MTU 是指在传输数据过程中容许报文的最大长度),如果大于最大传输单元,那么就调用 ip_fragment() 函数对数据包进行分片操作。
  • 如果数据包不能进行分片操作,那么设置 DF(Don't Fragment) 位为 1。
  • 设置 IP 数据包的 ID(标识符)。
  • 计算 IP 头部 的校验和。
  • 通过网卡设施把数据包发送进来,个别通过调用 dev_queue_xmit() 函数。

ip_queue_xmit2() 函数会持续设置 IP 头部 其余字段的值,而后调用 dev_queue_xmit() 函数把数据包发送进来。

当然还要判断发送的数据包长度是否大于最大传输单元,如果大于最大传输单元,那么就须要对数据包进行分片操作。数据分片是指把要发送的数据包宰割成多个以最大传输单元为最大长度的数据包,而后再把这些数据包发送进来。

IP 数据包的接管

IP 数据包的接管是通过 ip_rcv() 函数实现的,当网卡接管到数据包后,会上送到内核协定栈的链路层,链路层会依据链路层协定(如以太网协定)解析数据包。而后再将解析后的数据包通过调用 ip_rcv() 函数上送到网络层的 IP 协定ip_rcv() 函数的实现如下:

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt)
{
    struct iphdr *iph = skb->nh.iph; // 获取数据包的 IP 头部

    if (skb->pkt_type == PACKET_OTHERHOST) // 如果不是发送给本机的数据包, 则丢掉这个包
        goto drop;
    ...
    // 判断数据包的长度是否非法
    if (skb->len < sizeof(struct iphdr) || skb->len < (iph->ihl<<2))
        goto inhdr_error;

    // 1. 判断头部长度是否非法
    // 2. IP 协定的版本是否非法
    // 3. IP 头部的校验和是否正确
    if (iph->ihl < 5 || iph->version != 4 || ip_fast_csum((u8 *)iph, iph->ihl) != 0)
        goto inhdr_error;

    {__u32 len = ntohs(iph->tot_len);
        if (skb->len < len || len < (iph->ihl<<2)) // 数据包的长度是否非法
            goto inhdr_error;

        __skb_trim(skb, len);
    }

    // 持续调用 ip_rcv_finish() 函数解决数据包
    return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);

inhdr_error:
    IP_INC_STATS_BH(IpInHdrErrors);
drop:
    kfree_skb(skb);
out:
    return NET_RX_DROP;
}

ip_rcv() 函数的次要工作就是验证 IP 头部 各个字段的值是否非法,如果不非法就将数据包抛弃,否则就调用 ip_rcv_finish() 函数持续解决数据包。

ip_rcv_finish() 函数的实现如下:

static inline int ip_rcv_finish(struct sk_buff *skb)
{
    struct net_device *dev = skb->dev;
    struct iphdr *iph = skb->nh.iph;

    // 依据源 IP 地址、指标 IP 地址和服务类型查找路由信息
    if (skb->dst == NULL) {if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))
            goto drop;
    }
    ...

    // 如果是发送给本机的数据包将会调用 ip_local_deliver() 解决
    return skb->dst->input(skb);
}

ip_rcv_finish() 函数的实现比较简单,首先以 源 IP 地址 指标 IP 地址 服务类型 作为参数调用 ip_route_input() 函数查找对应的路由信息。

而后通过调用路由信息的 input() 办法解决数据包,如果是发送给本机的数据包 input() 办法将会指向 ip_local_deliver() 函数。

咱们接着剖析 ip_local_deliver() 函数:

int ip_local_deliver(struct sk_buff *skb)
{
    struct iphdr *iph = skb->nh.iph;

    // 如果是一个 IP 数据包的分片
    if (iph->frag_off & htons(IP_MF|IP_OFFSET)) {skb = ip_defrag(skb); // 将分片组装成真正的数据包,如果胜利将会返回组装后的数据包
        if (!skb)
            return 0;
    }

    // 持续调用 ip_local_deliver_finish() 函数解决数据包
    return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
                   ip_local_deliver_finish);
}

ip_local_deliver() 函数首先判断数据包是否是一个分片,如果是分片的话,就调用 ip_defrag() 函数对分片进行重组操作。重组胜利的话,会返回重组后的数据包。接着调用 ip_local_deliver_finish() 对数据包进行解决。

ip_local_deliver_finish() 函数的实现如下:

static inline int ip_local_deliver_finish(struct sk_buff *skb)
{
    struct iphdr *iph = skb->nh.iph;

    skb->h.raw = skb->nh.raw + iph->ihl*4; // 设置传输层头部(如 TCP/UDP 头部)

    {int hash = iph->protocol & (MAX_INET_PROTOS - 1); // 传输层协定对应的 hash 值
        struct sock *raw_sk = raw_v4_htable[hash];
        struct inet_protocol *ipprot;
        int flag;
        ...

        // 通过 hash 值找到传输层协定的处理函数
        ipprot = (struct inet_protocol *)inet_protos[hash]; 
        flag = 0;

        if (ipprot != NULL) {
            if (raw_sk == NULL 
                && ipprot->next == NULL 
                && ipprot->protocol == iph->protocol) 
            {
                // 调用传输层的数据包处理函数解决数据包
                return ipprot->handler(skb, (ntohs(iph->tot_len) - iph->ihl*4));
            } else {flag = ip_run_ipprot(skb, iph, ipprot, (raw_sk != NULL));
            }
        }
        ...
    }
    return 0;
}

ip_local_deliver_finish() 函数的次要工作就是依据下层协定(传输层)的类型,而后从 inet_protos 数组中找到其对应的数据包处理函数,而后通过此数据包处理函数解决数据。

也就是说,IP 层对数据包的正确性验证实现和重组后,会将数据包上送给传输层去解决。对于 TCP 协定 来说,数据包处理函数对应的是 tcp_v4_rcv()

咱们的公众号

正文完
 0