在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