全局结构体变量tcp_hashinfo,成员listening_hash为保存处于监听状态(TCP_LISTEN)的TCP套接口哈希链表。由于监听套接口需要被频繁的读写,内核将其对其到一行高速缓存的长度,以便快速访问。
struct inet_hashinfo tcp_hashinfo;
#define INET_LHTABLE_SIZE 32
struct inet_hashinfo {
struct inet_listen_hashbucket listening_hash[INET_LHTABLE_SIZE]
____cacheline_aligned_in_smp;
};
在函数tcp_init中初始化监听hash队列,内核将监听队列分为32个哈希桶bucket,每个哈希桶由独立的保护锁和链表,此结构通过减小锁的粒度,增加并行处理的可能。
void __init tcp_init(void)
{
inet_hashinfo_init(&tcp_hashinfo);
}
void inet_hashinfo_init(struct inet_hashinfo *h)
{
for (i = 0; i < INET_LHTABLE_SIZE; i++) {
spin_lock_init(&h->listening_hash[i].lock);
INIT_HLIST_HEAD(&h->listening_hash[i].head);
}
}
在应用层套接口函数listen调用后,内核执行inet_listen,之后通过函数inet_csk_listen_start调用具体协议的哈希hash函数,将套接口添加到哈希链表中。对于TCP而言,此处的hash函数为inet_hash。
struct proto tcp_prot = {
.name = "TCP",
.hash = inet_hash,
.unhash = inet_unhash,
}
int inet_csk_listen_start(struct sock *sk, int backlog)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct inet_sock *inet = inet_sk(sk);
sk_state_store(sk, TCP_LISTEN);
if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
inet->inet_sport = htons(inet->inet_num);
err = sk->sk_prot->hash(sk);
}
}
inet_hash简单的封装了__inet_hash函数。如下,如果开启了IPV6,并且启用了端口重用,将此套接口添加在监听套接口桶的链表末尾;否则,添加到链表头部。哈希桶的选择由函数inet_sk_listen_hashfn的返回值决定,其为:
int __inet_hash(struct sock *sk, struct sock *osk)
{
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
struct inet_listen_hashbucket *ilb;
ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];
if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport &&
sk->sk_family == AF_INET6)
hlist_add_tail_rcu(&sk->sk_node, &ilb->head);
else
hlist_add_head_rcu(&sk->sk_node, &ilb->head);
}
另外,对于SOCK_RAW套接口类型,内核使用其协议号protocol计算得到的哈希值作为索引,添加到监听套接口链表中。
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
struct sock *sk;
struct inet_sock *inet;
if (SOCK_RAW == sock->type)
inet->inet_num = protocol;
if (inet->inet_num) {
inet->inet_sport = htons(inet->inet_num);
err = sk->sk_prot->hash(sk);
}
}
在网络数据包进入到TCP的四层协议入口函数tcp_v4_rcv时,需要判断本机是否有套接口可处理此数据包(__inet_lookup_skb函数),这里首先是查询tcp_hashinfo结构中的成员ehash链表(包括所有状态在TCP_ESTABLISHED <= sk->sk_state < TCP_CLOSE之间的套接口),其次,在ehash中找不到的情况下,才是查找监听套接口链表,函数__inet_lookup_listener如下。
由函数inet_lhashfn可知,监听套接口队列listening_hash使用数据包的目的端口号(本地监听端口号)计算的hash值为索引得到具体的哈希桶,需要注意的是listening_hash的长度有限(32),可能有太多的不同端口号的套接口在获得的哈希桶内。在随后函数compute_score,将会看到计算套接口分值前,先行判断套接口监听端口与数据包目的端口是否相等。
struct sock *__inet_lookup_listener(struct net *net, struct inet_hashinfo *hashinfo, ...)
{
unsigned int hash = inet_lhashfn(net, hnum);
struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
}
接下来__inet_lookup_listener函数,遍历哈希桶链表,找到合适的套接口。首先,如果当前正在遍历的套接口score大于历史分值hiscore,并且开启了端口重用,将由当前套接口的端口重用组中选取一个套接口作为最终的套接口返回,见函数reuseport_select_sock。再者,如果没有开启端口重用,继续遍历寻找分值更高的套接口,直到遍历结束。
最后一种情况是,虽然开启了端口重用,但是在访问端口重用组之时,当前遍历的套接口还没有加入的组中来,将导致第一步的时候未能获得端口重用组中的套接口(reuseport_select_sock返回空),后续会逐个遍历到端口重用组之中的所有套接口,而它们的分值都是相同的,选择最后一个reciprocal_scale函数返回值为0的套接口。
struct sock *__inet_lookup_listener()
{
sk_for_each_rcu(sk, &ilb->head) {
score = compute_score(sk, net, hnum, daddr, dif, sdif, exact_dif);
if (score > hiscore) {
reuseport = sk->sk_reuseport;
if (reuseport) {
phash = inet_ehashfn(net, daddr, hnum, saddr, sport);
result = reuseport_select_sock(sk, phash, skb, doff);
if (result) return result;
matches = 1;
}
result = sk; hiscore = score;
} else if (score == hiscore && reuseport) {
matches++;
if (reciprocal_scale(phash, matches) == 0) result = sk;
phash = next_pseudo_random32(phash);
}
}
return result;
}
在遍历监听套接口hash桶的过程中,内核为其中的每个套接口计算分值,函数compute_score。前提是此套接口必须与数据包入接口所在的网络命名空间一致,并且其监听的本地端口与数据包的目的端口相同,而且还用不是仅监听ipv6的套接口(我们这里是在处理ipv4数据包)。
如果套接口为PF_INET协议族,其初始分值为2,否则为1。
如果套接口明确指定了其监听的本地IP地址,即不是绑定在INADDR_ANY定义的任意地址上,并且此套接口监听的本地IP地址与数据包的目的IP地址相同,此套接口分值加4;
如果应用层使用setsockopt系统调用的选项SO_BINDTODEVICE指定了套接口绑定的本地接口,并且其与数据包的入接口或者入接口所属的VRF接口相同,此套接口分值增加4。如果系统不允许一个套接口监听在所有的VRF上(/proc/sys/net/ipv4/tcp_l3mdev_accept值为0),对于仅监听在特定VRF的套接口,其所绑定的本地接口必须与数据包的入接口或者入接口所属的VRF接口相同。
如果应用层使用setsockopt系统调用的选项SO_INCOMING_CPU指定了套接口所在的处理器核,并且其与当前的处理器核ID相同,此套接口分值增加1,可见此项在选择套接口时所占的比重没有其它几项高。套接口所在的处理器核在用户层设置之后不是保持不变,而是在处理数据包之后,将会得到更新。
以上可见,监听套接口指定的参数越明确得到的分值越高。
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)
{
if (net_eq(sock_net(sk), net) && inet->inet_num == hnum && !ipv6_only_sock(sk)) {
__be32 rcv_saddr = inet->inet_rcv_saddr;
score = sk->sk_family == PF_INET ? 2 : 1;
if (rcv_saddr) {
if (rcv_saddr != daddr) return -1;
score += 4;
}
if (sk->sk_bound_dev_if || exact_dif) {
bool dev_match = (sk->sk_bound_dev_if == dif || sk->sk_bound_dev_if == sdif);
if (exact_dif && !dev_match) return -1;
if (sk->sk_bound_dev_if && dev_match) score += 4;
}
if (sk->sk_incoming_cpu == raw_smp_processor_id())
score++;
}
return score;
}
PROC文件/proc/net/tcp,显示监听和建立状态的套接口。
int tcp_proc_register(struct net *net, struct tcp_seq_afinfo *afinfo)
{
afinfo->seq_ops.start = tcp_seq_start;
afinfo->seq_ops.next = tcp_seq_next;
afinfo->seq_ops.stop = tcp_seq_stop;
p = proc_create_data(afinfo->name, S_IRUGO, net->proc_net, afinfo->seq_fops, afinfo);
}
监听状态的套接口显示在前,其后是已建立状态的套接口:
$ cat /proc/net/tcp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:14EB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 102 0 3959480 1 0000000000000000 100 0 0 10 0
1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 20400 1 0000000000000000 100 0 0 10 0
2: 0100007F:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000 124 0 20902 1 0000000000000000 100 0 0 10 0
3: 7001A8C0:0016 6E01A8C0:C911 01 00000000:00000000 02:0000C52E 00000000 0 0 3525368 2 0000000000000000 24 4 29 10 -1
$
内核版本 4.15.0