TCP套接口选择

在TCP接收函数tcp_v4_rcv中,由__inet_lookup_skb执行报文所属套接口的查找任务。

int tcp_v4_rcv(struct sk_buff *skb)
{
    int sdif = inet_sdif(skb);

    th = (const struct tcphdr *)skb->data;
    iph = ip_hdr(skb);
lookup:
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
                   th->dest, sdif, &refcounted);
    if (!sk)
        goto no_tcp_socket;

以下为__inet_lookup_skb的实现,其封装了函数__inet_lookup。

static inline struct sock *__inet_lookup_skb(struct inet_hashinfo *hashinfo,
       struct sk_buff *skb, int doff, const __be16 sport, const __be16 dport,
       const int sdif, bool *refcounted)
{
    struct sock *sk = skb_steal_sock(skb);
    const struct iphdr *iph = ip_hdr(skb);

    *refcounted = true;
    if (sk) return sk;

    return __inet_lookup(dev_net(skb_dst(skb)->dev), hashinfo, skb,
                 doff, iph->saddr, sport,
                 iph->daddr, dport, inet_iif(skb), sdif,
                 refcounted);

由以下函数__inet_lookup可知,内核会首先查找连接建立状态的套接口(__inet_lookup_established),在没有命中的情况下,才会查找监听套接口(__inet_lookup_listener)。

static inline struct sock *__inet_lookup(struct net *net,
                     struct inet_hashinfo *hashinfo, struct sk_buff *skb, int doff,
                     const __be32 saddr, const __be16 sport, const __be32 daddr, const __be16 dport,
                     const int dif, const int sdif, bool *refcounted)
{
    u16 hnum = ntohs(dport);
    struct sock *sk;

    sk = __inet_lookup_established(net, hashinfo, saddr, sport,
                       daddr, hnum, dif, sdif);
    *refcounted = true;
    if (sk)
        return sk;
    *refcounted = false;
    return __inet_lookup_listener(net, hashinfo, skb, doff, saddr,
                      sport, daddr, hnum, dif, sdif);

先来看一下监听端口的查找,如下函数__inet_lookup_listener,其分为2个部分,取目的地址和端口号的哈希值,由函数inet_lhash2_lookup遍历对应的监听端口链表,如果没有找到合适的套接口,放宽监听地址,哈希值取INADDR_ANY(0x00000000)和目的端口生成,再次遍历此哈希值对应的链表,查找监听套接口。

struct sock *__inet_lookup_listener(struct net *net, struct inet_hashinfo *hashinfo,
                    struct sk_buff *skb, int doff, const __be32 saddr, __be16 sport,
                    const __be32 daddr, const unsigned short hnum, const int dif, const int sdif)
{
    struct inet_listen_hashbucket *ilb2;

    hash2 = ipv4_portaddr_hash(net, daddr, hnum);
    ilb2 = inet_lhash2_bucket(hashinfo, hash2);

    result = inet_lhash2_lookup(net, ilb2, skb, doff,
                    saddr, sport, daddr, hnum, dif, sdif);
    if (result) goto done;

    /* Lookup lhash2 with INADDR_ANY */
    hash2 = ipv4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
    ilb2 = inet_lhash2_bucket(hashinfo, hash2);

    result = inet_lhash2_lookup(net, ilb2, skb, doff,
                    saddr, sport, htonl(INADDR_ANY), hnum, dif, sdif);
done:
    if (unlikely(IS_ERR(result))) return NULL;
    return result;

如下函数inet_lhash2_lookup,遍历监听哈希链表(ilb2->head),为每个监听套接口计算分值(稍后介绍计算方法),找到分值最高的套接口。对于端口重用的情况,由函数reuseport_select_sock选择其中的某个套接口。

static struct sock *inet_lhash2_lookup(struct net *net,
                struct inet_listen_hashbucket *ilb2, struct sk_buff *skb, int doff,
                const __be32 saddr, __be16 sport, const __be32 daddr, const unsigned short hnum,
                const int dif, const int sdif)
{
    bool exact_dif = inet_exact_dif_match(net, skb);
    struct inet_connection_sock *icsk;

    inet_lhash2_for_each_icsk_rcu(icsk, &ilb2->head) {
        sk = (struct sock *)icsk;
        score = compute_score(sk, net, hnum, daddr, dif, sdif, exact_dif);
        if (score > hiscore) {
            if (sk->sk_reuseport) {
                phash = inet_ehashfn(net, daddr, hnum, saddr, sport);
                result = reuseport_select_sock(sk, phash, skb, doff);
                if (result) return result;
            }
            result = sk;
            hiscore = score;
        }
    }
    return result;

计分的前提条件: 套接口所在网络命名空间必须与报文接收设备所在命名空间相同,而且套接口的监听端口与报文目的端口相同,最后,套接口需要支持ipv4连接。

1) 套接口指定的监听IP地址(sk_rcv_saddr)与报文的目的地址不同,返回-1。
2) 套接口绑定了设备接口(sk_bound_dev_if),其与报文接收的接口不同,返回-1。
3) 套接口的协议栈为IPv4,分值为2,否则分值为1,可见IPv6的分值小于IPv4。
4) 套接口记录的最近一次接收报文使用的CPU与当前CPU相同,分值加一。可使用套接口选项SO_INCOMING_CPU指定sk_incoming_cpu的值,但仅生效一次。

static inline int compute_score(struct sock *sk, struct net *net,
                const unsigned short hnum, const __be32 daddr,
                const int dif, const int sdif, bool exact_dif)
{
    int score = -1;

    if (net_eq(sock_net(sk), net) && sk->sk_num == hnum && !ipv6_only_sock(sk)) {
        if (sk->sk_rcv_saddr != daddr)
            return -1;
        if (!inet_sk_bound_dev_eq(net, sk->sk_bound_dev_if, dif, sdif))
            return -1;

        score = sk->sk_family == PF_INET ? 2 : 1;
        if (sk->sk_incoming_cpu == raw_smp_processor_id())
            score++;
    }
    return score;

以下,如果使用套接口选项SO_REUSEPORT,多个套接口监听在相同的地址端口上,通过BPF程序选择其中的某个套接口,如果不存在BPF程序,或者其选择套接口失败,通过哈希值选择套接口。

struct sock *reuseport_select_sock(struct sock *sk, u32 hash, struct sk_buff *skb, int hdr_len)
{   
    struct sock_reuseport *reuse;
    struct bpf_prog *prog;

    rcu_read_lock();
    reuse = rcu_dereference(sk->sk_reuseport_cb);
    
    prog = rcu_dereference(reuse->prog);
    socks = READ_ONCE(reuse->num_socks);
    if (likely(socks)) {
        if (!prog || !skb) goto select_by_hash;
        
        if (prog->type == BPF_PROG_TYPE_SK_REUSEPORT)
            sk2 = bpf_run_sk_reuseport(reuse, sk, prog, skb, hash);
        else
            sk2 = run_bpf_filter(reuse, socks, prog, skb, hdr_len);

select_by_hash:
        if (!sk2) sk2 = reuse->socks[reciprocal_scale(hash, socks)];
    }
out:
    rcu_read_unlock();
    return sk2;

最后看一下连接已经建立的套接口的查找,如下函数__inet_lookup_established,与监听套接口查找不同,连接建立后的套接口的地址中不会有通配符的情况(INADDR_ANY),根据源/目的地址,端口号和网络命名空间获取哈希值(inet_ehashfn),由对应的slot值求得在套接口链表的首地址(ehash[slot])。之后遍历此链表,INET_MATCH宏比较当前遍历的套接口与报文是否满足以下条件:

1) 确认哈希值相同(由inet_ehashfn计算);
2) 两者的源和目的地址都相同,通过将源和目的地址组成__addrpair结构进行比较;
3) 两者的源和目的端口都相同,通过将源和目的地址组成__portpair结构进行比较;
4) 两者的网络命名空间相同;
5) 套接口绑定设备与接收报文的网络设备相同;

以上的条件都满足时,但是套接口的引用计数为零,函数返回空。否则,套接口引用计数不为零,并且执行引用计数的增加,之后再进行一次相同的比较,确认引用计数增加前套接口没有被修改,否则,重新遍历链表。两次INET_MATCH都成立的话,返回找到的套接口。

struct sock *__inet_lookup_established(struct net *net, struct inet_hashinfo *hashinfo,
                  const __be32 saddr, const __be16 sport, const __be32 daddr, const u16 hnum,
                  const int dif, const int sdif)
{   
    INET_ADDR_COOKIE(acookie, saddr, daddr);
    const __portpair ports = INET_COMBINED_PORTS(sport, hnum);
    const struct hlist_nulls_node *node;
    unsigned int hash = inet_ehashfn(net, daddr, hnum, saddr, sport);
    unsigned int slot = hash & hashinfo->ehash_mask;
    struct inet_ehash_bucket *head = &hashinfo->ehash[slot];

begin:
    sk_nulls_for_each_rcu(sk, node, &head->chain) {
        if (sk->sk_hash != hash)
            continue;
        if (likely(INET_MATCH(sk, net, acookie, saddr, daddr, ports, dif, sdif))) {
            if (unlikely(!refcount_inc_not_zero(&sk->sk_refcnt)))
                goto out;
            if (unlikely(!INET_MATCH(sk, net, acookie, saddr, daddr, ports, dif, sdif))) {
                sock_gen_put(sk);
                goto begin;
            }
            goto found;
        }
    }

如果在链表遍历完成之后,最后的node节点的值不等与遍历链表的位置值,表明可能遍历的元素被移动到了另外的位置,需要重新进行原链表的遍历。

    /*
     * if the nulls value we got at the end of this lookup is not the expected one, we must restart lookup.
     * We probably met an item that was moved to another chain.
     */
    if (get_nulls_value(node) != slot) goto begin;
out:
    sk = NULL;
found:
    return sk;

内核版本 5.0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章