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

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