在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