tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二

内核版本:3.4.39

上篇我们分析了UDP套接字如何接收数据的流程,最终它是在内核套接字的接收队列里取出报文,剩下的问题就是谁会去写入这个队列,当然,这部分工作由内核来完成,本篇剩下的文章主要分析内核网络层收到UDP报文后如何将报文插入到对应套接字的接收队列里面。

我们直到网络层到传输层的最终的接口是ip_local_deliver_finish,下面是它的代码:

static int ip_local_deliver_finish(struct sk_buff *skb)
{
    struct net *net = dev_net(skb->dev);
    /* 拉出IP报文首部,因为马上就要脱离IP层,进入传输层了。 */
    __skb_pull(skb, ip_hdrlen(skb));
    /* 设置传输层首部地址 */
    skb_reset_transport_header(skb);
    rcu_read_lock();
    {
        /* 得到传输层协议 */
        int protocol = ip_hdr(skb)->protocol;
        int hash, raw;
        const struct net_protocol *ipprot;
    resubmit:
        /* 将数据包传递给对应的原始套接字 */
        raw = raw_local_deliver(skb, protocol);
        /* 根据传输协议确定对应的inet协议 */
        hash = protocol & (MAX_INET_PROTOS - 1);
        ipprot = rcu_dereference(inet_protos[hash]);
        if (ipprot != NULL) {
            /* 找到了匹配传输层的协议 */
            int ret;
            /* 检查名称空间是否匹配 */
            if (!net_eq(net, &init_net) && !ipprot->netns_ok) {
                if (net_ratelimit())
                    printk("%s: proto %d isn't netns-ready\n",
                        __func__, protocol);
                kfree_skb(skb);
                goto out;
            }
            /* 协议的安全策略检查 */
            if (!ipprot->no_policy) {
                if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                    kfree_skb(skb);
                    goto out;
                }
                nf_reset(skb);
            }
            /* 将数据包传递给传输层处理 */
            ret = ipprot->handler(skb);
            if (ret < 0) {
                protocol = -ret;
                goto resubmit;
            }
            IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
        } else {
            /* 没有对应的传输层协议 */
            if (!raw) {
                /* 若没有匹配的原始套接字,则进行安全策略检查 */
                if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                    /* 若没有对应的安全策略,则使用ICMP返回不可达错误 */
                    IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS);
                    icmp_send(skb, ICMP_DEST_UNREACH,
                          ICMP_PROT_UNREACH, 0);
                }
            } else
                IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
            kfree_skb(skb);
        }
    }
 out:
    rcu_read_unlock();
    return 0;
}

内核通过调用ipprot->handler(skb)将数据包传递给了正确的传输层协议。对于IPv4协议来说,其传输层协议的处理函数的handler是在inet_init中添加的。下面是inet_init中的部分代码:

 /* 添加ICMP协议 */
    if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
        printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");
    /* 添加UDP协议 */
    if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
        printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");
    /* 添加TCP协议 */
    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
        printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n");
#ifdef CONFIG_IP_MULTICAST
    /* 添加IGMP协议 */
    if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
        printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n");
#endif

通过调用inet_add_protocol函数,传输层将自己的处理函数添加到了inet_protos中,这样就可以在ip_local_deliver_finish中调用对应的传输层的处理函数了。

inet_init中的另一部分代码如下:

  for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
        inet_register_protosw(q);

这部分代码用于注册AF_INET的各种协议,如UDP、TCP等。inet_add_protocol面向的是底层接口,而inet_register_protosw面向的是上层应用,所以将其分为了两个结构。 

UDP协议的面向底层接口的处理结构为:

static const struct net_protocol udp_protocol = {
    .handler = udp_rcv,
    .err_handler = udp_err,
    .gso_send_check = udp4_ufo_send_check,
    .gso_segment = udp4_ufo_fragment,
    .no_policy = 1,
    .netns_ok = 1,
};

因此,如果是UDP数据包,会依次进入udp_rcv→__udp4_lib_rcv,下面来看看__udp4_lib_rcv的相关代码:

int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
           int proto)
{
    struct sock *sk;
    struct udphdr *uh;
    unsigned short ulen;
    struct rtable *rt = skb_rtable(skb);
    __be32 saddr, daddr;
    struct net *net = dev_net(skb->dev);
    /* 校验数据包至少要有UDP首部大小 */
    if (!pskb_may_pull(skb, sizeof(struct udphdr)))
        goto drop;        /* No space for header. */
    /* 得到UDP首部指针 */
    uh   = udp_hdr(skb);
    /* 得到UDP数据包长度、源地址、目的地址 */
    ulen = ntohs(uh->len);
    saddr = ip_hdr(skb)->saddr;
    daddr = ip_hdr(skb)->daddr;
    /* 如果UDP数据包长度超过数据包的实际长度,则出错 */
    if (ulen > skb->len)
        goto short_packet;
    /*
    判断协议是否为UDP协议。
    也许有的读者会觉得很奇怪,为什么在UDP的接收函数中还要判断协议是否为UDP?
    因为这个函数还用于处理UDPLITE协议。
     */
    if (proto == IPPROTO_UDP) {
        /* 如果是UDP协议,则将数据包的长度更新为UDP指定的长度,并更新校验和 */
        if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen))
            goto short_packet;
        /* 因为前面的操作可能会导致skb内存变化,所以需要重新获得UDP首部指针 */
        uh = udp_hdr(skb);
    }
    /* 初始化UDP校验和 */
    if (udp4_csum_init(skb, uh, proto))
        goto csum_error;
    /* 如果路由标志位广播或多播,则表明该UDP数据包为广播或多播 */
    if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
        return __udp4_lib_mcast_deliver(net, skb, uh,
               saddr, daddr, udptable);
    /* 确定匹配的UDP套接字 */
    sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
    if (sk != NULL) {
        /* 找到了匹配的套接字 */
        /* 将数据包加入到UDP的接收队列 */
        int ret = udp_queue_rcv_skb(sk, skb);
        sock_put(sk);
        /* a return value > 0 means to resubmit the input, but
         * it wants the return to be -protocol, or 0
         */
        if (ret > 0)
            return -ret;
        return 0;
    }
    /* 进行xfrm策略检查 */
    if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
        goto drop;
    /* 重置netfilter信息 */
    nf_reset(skb);
    /* 检查UDP检验和 */
    if (udp_lib_checksum_complete(skb))
        goto csum_error;
    /* 若不知道匹配的UDP套接字,则发送ICMP错误消息 */
    UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
    icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
    /*
     * Hmm.  We got an UDP packet to a port to which we
     * don't wanna listen.  Ignore it.
     */
    kfree_skb(skb);
    return 0;
    /* 错误处理 */
    ……
}

下面来看一下如何匹配UDP套接字,请看__udp4_lib_lookup_skb→__udp4_lib_lookup函数,代码如下: 

static struct sock *__udp4_lib_lookup(struct net *net, __be32 saddr,
        __be16 sport, __be32 daddr, __be16 dport,
        int dif, struct udp_table *udptable)
{
    struct sock *sk, *result;
    struct hlist_nulls_node *node;
    unsigned short hnum = ntohs(dport);
    /* 使用目的端口确定hash桶索引 */
    unsigned int hash2, slot2, slot = udp_hashfn(net, hnum, udptable->mask);
    struct udp_hslot *hslot2, *hslot = &udptable->hash[slot];
    int score, badness;
    rcu_read_lock();
    /* 若该桶的套接字个数多于10个,则需要再次定位 */
    if (hslot->count > 10) {
        /* 使用目的地址和目的端口确定hash桶索引 */
        hash2 = udp4_portaddr_hash(net, daddr, hnum);
        slot2 = hash2 & udptable->mask;
        /*
        UDP套接字表维护了两个hash表:
         第一个hash表,使用端口来索引。
         第二个hash表,使用地址+端口来索引。
       在进行UDP套接字匹配的时候,优先使用第一个hash表,因为第一个hash表使用的是端口进行散
       列索引,那么只要端口相同,无论是监听的指定IP还是任意IP,都可以在一个桶中进行匹配。但
       是由于端口只有65535种可能,所以可能导致不够分散,一个桶的套接字个数会比较多。而第二个
       hash表是使用地址+端口来索引的,因此理论上套接字的分布会比第一个hash表更加分散。
       因此当第一个hash表对应桶的套接字多于10个时,内核会尝试去第二个hash表中进行匹配查找。
         */
        hslot2 = &udptable->hash2[slot2];
        /* 尽管第二个hash表理论上会比第一个hash表分散,但是如果实际上第二个表的桶中套接字
        个数大于第一个表的桶中套接字个数,那么这时还是利用第一个hash表进行匹配 */
        if (hslot->count < hslot2->count)
            goto begin;
        /* 在第二个hash表的桶中匹配查找套接字 */
        result = udp4_lib_lookup2(net, saddr, sport,
                      daddr, hnum, dif,
                      hslot2, slot2);
        if (!result) {
            /* 若利用指定的IP和端口在该桶中没能找到匹配的套接字,则通常使用任意IP+端口来进行
               散列索引 */
            hash2 = udp4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
            slot2 = hash2 & udptable->mask;
            hslot2 = &udptable->hash2[slot2];
            /* 还是要与第一个hash桶中的个数进行比较 */
            if (hslot->count < hslot2->count)
                goto begin;
            /* 在第二个hash表中使用任意IP+端口进行匹配查找 */
            result = udp4_lib_lookup2(net, saddr, sport,
                        htonl(INADDR_ANY), hnum, dif,
                        hslot2, slot2);
        }
        rcu_read_unlock();
        return result;
    }
begin:
    result = NULL;
    badness = -1;
    /* 在第一个hash表的桶中进行查找 */
    sk_nulls_for_each_rcu(sk, node, &hslot->head) {
        /* 计算该套接字的匹配得分 */
        score = compute_score(sk, net, saddr, hnum, sport,
                      daddr, dport, dif);
        /* 保证匹配得分最高的套接字为最终结果 */
        if (score > badness) {
            result = sk;
            badness = score;
        }
    }
    /*
    检查在查找的过程中,是否遇到了某个套接字被移到另外一个桶内的情况。
    这时,需要重新进行匹配。
     */
    if (get_nulls_value(node) != slot)
        goto begin;
    /* 找到了匹配的套接字 */
    if (result) {
        /* 增加套接字引用计数 */
        if (unlikely(!atomic_inc_not_zero_hint(&result->sk_refcnt, 2)))
            result = NULL;
        /* 再次计算套接字得分,如小于最大分数,则重新匹配查找。之所以做二次检查,也是为了防止在
           匹配与增加引用的过程中,套接字发生变化。 */
        else if (unlikely(compute_score(result, net, saddr, hnum, sport,
                daddr, dport, dif) < badness)) {
            sock_put(result);
            goto begin;
        }
    }
    rcu_read_unlock();
    return result;
}

从上面的代码中可以看到,匹配UDP套接字的关键在于对应套接字的匹配得分。第一个hash表的得分计算函数为compute_score。

static inline int compute_score(struct sock *sk, struct net *net, __be32 saddr,
            unsigned short hnum,
            __be16 sport, __be32 daddr, __be16 dport, int dif)
{
    int score = -1;
    /* 比较名称空间,端口等 */
    if (net_eq(sock_net(sk), net) && udp_sk(sk)->udp_port_hash == hnum &&
            !ipv6_only_sock(sk)) {
        struct inet_sock *inet = inet_sk(sk);
        /* 若套接字指明为PF_INET,则加1分 */
        score = (sk->sk_family == PF_INET ? 1 : 0);
        /* 套接字绑定了接收地址 */
        if (inet->inet_rcv_saddr) {
            /* 如果数据包的目的地址与绑定接收地址不符,则分数为-1,相同则增加2分。 */
            if (inet->inet_rcv_saddr != daddr)
                return -1;
            score += 2;
        }
        /* 套接字设置了对端目的地址 */
        if (inet->inet_daddr) {
            /* 如果数据包的源地址与设置的目的地址不同,则分数为-1,相同则增加2分 */
            if (inet->inet_daddr != saddr)
                return -1;
            score += 2;
        }
        /* 套接字设置了对端目的端口 */
        if (inet->inet_dport) {
            /* 如果数据包的源端口与设置的目的端口不同,则分数为-1,相同则增加2分 */
            if (inet->inet_dport != sport)
                return -1;
            score += 2;
        }
        /* 套接字绑定了网卡 */
        if (sk->sk_bound_dev_if) {
            /* 如果接受数据包的网卡与绑定网卡不同,则分数为-1,相同则增加2分 */
            if (sk->sk_bound_dev_if != dif)
                return -1;
            score += 2;
        }
    }
    return score;
}

 

对于第二个hash,其匹配分数计算函数为compute_score2,算法与compute_score基本相同。总的来说UDP的套接字匹配有以下几个条件:

·接收端口:必须匹配。

·接收地址:如绑定了则必须匹配,分值为2分。

·对端目的地址:如设置了则必须匹配,分值为2分。

·对端目的端口:如设置了则必须匹配,分值为2分。

·网卡:如绑定了则必须匹配,分值为2分。

·套接字设置了PF_INET协议族,分值为1分。

根据上面的规则,匹配分值最高的套接字就为选中的UDP套接字,然后内核会将这个数据包加入到该UDP套接字的接收队列中。也就是说,即使数据包可以匹配多个UDP套接字(这是很有可能的),但是最终也只有一个最匹配的套接字会被选中,并且只有这个套接字可以收到数据包。

有一些开发人员想使用套接字的SO_REUSEADDR选项,让多个套接字绑定同一个地址或端口,然后让独立的线程或进程负责一个套接字的处理,希望利用这样的设计来提高服务的响应速度。这里面有个想当然的认为,当多个套接字负责同一个地址和端口的数据包接收时,它们可以分担负载。然而从上面的源码分析中,我们可以发现这样的设计方案是达不到预期效果的。因为内核在进行套接字的匹配时,对于绑定相同地址和端口的多个套接字,每次只会命中同一个套接字。结果在上面的设计中,只有一个套接字会收到数据包,也就说最后只有一个线程或进程在处理数据包。

不过Linux内核在3.9版本中引入了一个新的套接字选项SO_REUSEPORT用于解决上面的问题。当多个套接字绑定于同一个地址和端口时,并启用了SO_REUSEPORT时,内核会自动在这几个套接字之间做负载均衡,保证对应的数据包能尽量平均地分配到不同的套接字上。

参考文档:

1. 《Linux环境编程:从应用到内核》

2.  浅析Linux网络子系统(一) 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值