TCP连接建立系列 — 客户端的端口选取和重用
主要内容:connect()时的端口选取和端口重用。
内核版本:3.15.2
我的博客:http://blog.csdn.net/zhangskd
端口选取
connect()时本地端口是如何选取的呢?
如果用户已经绑定了端口,就使用绑定的端口。
如果用户没有绑定端口,则让系统自动选取,策略如下:
1. 获取端口的取值区间,以及区间内端口的个数。
2. 根据初始偏移量,从端口区间内的某个端口开始,遍历整个区间。
2.1 如果端口是保留的,直接跳过。
2.2 如果端口已经被使用了。
2.2.1 不允许复用已经被bind()的端口。
2.2.2 检查端口是否能被重用,可以的话就重用此端口。
2.3 如果端口没有被使用过,就选择此端口。
当没有端口可用时,会报如下错误:
-EADDRNOTAVAIL /* Cannot assign requested address */
包含两种场景:
1. 端口区间内没有未使用过的端口,且正在使用的端口都不允许复用。
2. 内存不够,无法创建端口的存储结构。
/* Bind a port for a connect operation and hash it. */ int inet_hash_connect (struct inet_timewait_death_row *death_row, struct sock *sk) { return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk), __inet_check_established, __inet_hash_nolisten); }
inet_hash_connect()参数的含义如下:
death_row:TIME_WAIT socket的管理结构。
inet_sk_port_offset():根据源IP、目的IP、目的端口,采用MD5计算出一个随机数,作为端口的初始偏移值。
__inet_check_established():判断正在使用中的端口是否允许重用。
__inet_hash_nolisten():根据四元组,计算sk在ehash哈希表中的索引,把sk链入ehash哈希表。
int __inet_hash_connect (struct inet_timewait_death_row *death_row, struct sock *sk, u32 port_offset, int (*check_established)(struct inet_timewait_death_row *, struct sock *, __u16, struct inet_timewait_sock **), int (*hash)(struct sock *sk, struct inet_timewait_sock *twp)) { struct inet_hashinfo *hinfo = death_row->hashinfo; /* tcp_hashinfo */ const unsigned short snum = inet_sk(sk)->inet_num; /* 本端端口 */ struct inet_bind_hashbucket *head; struct inet_bind_bucket *tb; int ret; struct net *net = sock_net(sk); int twrefcnt = 1; /* snum为0时,表示用户没有绑定端口,默认让系统自动选取端口 */ if (! snum) { int i, remaining, low, high, port; static u32 hint; /* 用于保存上次查找的位置 */ u32 offset = hint + port_offset; struct inet_timewait_sock *tw = NULL; /* 系统自动分配时,获取端口号的取值范围 */ inet_get_local_port_range(net, &low, &high); remaining = (high - low) + 1; /* 取值范围内端口号的个数 */ local_bh_disable(); for (i = 1; i <= remaining; i++) { /* 根据MD5计算得到的port_offset值,以及hint,获取范围内的一个端口 */ port = low + (i + offset) % remaining; /* 如果此端口号属于保留的,那么直接跳过 */ if (inet_is_reserved_local_port(port)) continue; /* 根据端口号,找到所在的哈希桶 */ head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)]; spin_lock(&head->lock); /* 锁住此哈希桶 */ /* 从头遍历哈希桶 */ inet_bind_bucket_for_each(tb, &head->chain) { /* 如果此端口已经被使用了 */ if (net_eq(ib_net(tb), net) && tb->port == port) { /* 不允许使用已经被bind()绑定的端口,无论此端口是否能够被复用 */ if (tb->fastreuse >= 0 || tb->fastreuseport >= 0) goto next_port; WARN_ON(hlist_empty(&tb->owners)); /* 检查端口是否允许重用 */ if (! check_established(death_row, sk, port, &tw)) goto ok; /* 成功,该端口可以被重复使用 */ goto next_port; /* 失败 */ } } /* 走到这里,表示该端口尚未被使用。 * 创建一个inet_bind_bucket实例,并把它加入到哈希桶中。 */ tb = inet_bind_bucket_create(hinfo->bind_bucket, cachep, net, head, port); /* 如果内存不够,则退出端口选择。 * 会导致connect()失败,返回-EADDRNOTAVAIL。 */ if (! tb) { spin_unlock(&head->lock); break; } tb->fastreuse = -1; tb->fastreuseport = -1; goto ok; next_port: spin_unlock(&head->lock); } /* end of for */ local_bh_enable(); /* 有两种可能:内存不够、端口区间内的端口号用光 */ return -EADDRNOTAVAIL; /* Cannot assign requested address */ ok: hint += i; /* 下一次connect()时,查找端口增加了这段偏移 */ /* Head lock still held and bh's disabled. * 把tb赋值给icsk->icsk_bind_hash,更新inet->inet_num,把sock链入tb->owners哈希链中。 * 更新该端口的绑定次数,系统总的端口绑定次数。 */ inet_bind_hash(sk, tb, port); /* 如果sk尚未链入ehash哈希表中 */ if (sk_unhashed(sk)) { inet_sk(sk)->inet_sport = htons(port); /* 保存本地端口 */ twrefcnt += hash(sk, tw); /* 把sk链入到ehash哈希表中,把tw从ehash表中删除 */ } if (tw) twrefcnt += inet_twsk_bind_unhash(tw, hinfo); /* 把tw从该端口的使用者链表中删除 */ spin_unlock(&head->lock); if (tw) { /* 把tw从tcp_death_row、ehash、bhash的哈希表中删除,更新tw的引用计数 */ inet_twsk_deschedule(tw, death_row); while (twrefcnt) { twrefcnt--; inet_twsk_put(tw); /* 释放tw结构体 */ } } ret = 0; goto out; } /* 走到这里,表示用户已经自己绑定了端口 */ head = &hinfo->bhash[inet_bhashfn(net, snum, hinfo->bhash_size)]; /* 端口所在的哈希桶 */ tb = inet_csk(sk)->icsk_bind_hash; /* 端口的存储实例 */ spin_lock_bh(&head->lock); /* 如果sk是此端口的使用者队列的第一个节点 */ if (sk_head(&tb->owners) == sk && ! sk->sk_bind_node.next) { hash(sk, NULL); /* 计算sk在ehash中的索引,赋值给sk->sk_hash,把sk链入到ehash表中 */ spin_unlock_bh(&head->lock); return 0; } else { spin_unlock(&head->lock); /* No definite answer... Walk to established hash table */ ret = check_established(death_row, sk, snum, NULL); /* 查看是否有可以重用的端口 */ out: local_bh_enable(); return ret; } }
根据四元组,计算sk在ehash哈希表中的索引,保存到sk->sk_hash中,然后把sk链入ehash哈希表。
int __inet_hash_nolisten(struct sock *sk, struct inet_timewait_sock *tw) { struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo; struct hlist_nulls_head *list; spinlock_t *lock; struct inet_ehash_bucket *head; int twrefcnt = 0; WARN_ON(! sk_unhashed(sk)); sk->sk_hash = inet_sk_ehashfn(sk); /* 根据四元组,计算在ehash哈希表中的索引 */ head = inet_ehash_bucket(hashinfo, sk->sk_hash); /* 根据索引,找到对应的哈希桶 */ list = &head->chain; lock = inet_ehash_lockp(hashinfo, sk->sk_hash); /* 根据索引,找到对应哈希桶的锁 */ spin_lock(lock); __sk_nulls_add_node_rcu(sk, list); /* 把sk->sk_null_node链入链表 */ if (tw) { /* 如果复用了TIME_WAIT sock的端口 */ WARN_ON(sk->sk_hash != tw->tw_hash); /* 把tw从ehash表中删除,返回值如果为1,表示释放锁之后,需要调用inet_twsk_put() */ twrefcnt = inet_twsk_unhash(tw); } spin_unlock(lock); sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1); /* 增加TCP协议的引用计数 */ return twrefcnt; }
通过源IP、目的IP、源端口、目的端口,计算得到一个32位的哈希值。
赋值给sk->sk_hash,作为索引,用于定位ehash中的哈希桶。
static unsigned int inet_sk_ehashfn(const struct sock *sk) { const struct inet_sock *inet = inet_sk(sk); const __be32 laddr = inet->inet_rcv_saddr; const __u16 lport = inet->inet_num; const __be32 faddr = inet->inet_daddr; const __be16 fport = inet->inet_dport; struct net *net = sock_net(sk); return inet_ehashfn(net, laddr, lport, faddr, fport); } static inline unsigned int inet_ehashfn (struct net *net, const __be32 laddr, const __u16 lport, const __be32 faddr, const __be16 fport) { return jhash_3words((__force __u32) laddr, (__force __u32) faddr, ((__u32) lport) << 16 | (__force __u32) fport, inet_ehash_secret + net_hash_mix(net)); } u32 inet_ehash_secret __read_mostly; /* inet_ehash_secret must be set exactly once */ void build_ehash_secret(void) { u32 rnd; do { get_random_bytes(&rnd, sizeof(rnd)); } while (rnd == 0); /* cmpxchg(void *ptr, unsigned long old, unsigned long new) * 比较*ptr和old: * 如果相等,则将new写入*ptr,返回old。 * 如果不相等,返回*ptr。 * 这里用于确保inet_ehash_secret只被写入一次。 */ cmpxchg(&inet_ehash_secret, 0, rnd); }
端口重用
__inet_check_established()用来检查已经在使用中的端口是否可以重用。
如果在ehash哈希表中没有找到一条四元组相同的连接,这个端口当然允许重用。
如果在ehash哈希表中找到一条完全一样的连接,即四元组相同、绑定的设备相同,
那么还要符合以下条件:
1. 连接的状态为TCP_TIME_WAIT。
2. 使用了TCP_TIMESTAMP选项。
3. 使用tcp_tw_reuse,并且此连接最近收到数据包的时间在1s以前。
/* called with local bh disabled */ static int __inet_check_established(struct inet_timewait_death_row *death_row, struct sock *sk, __u16 lport, struct inet_timewait_sock **twp) { struct inet_hashinfo *hinfo = death_row->hashinfo; struct inet_sock *inet = inet_sk(sk); __be32 daddr = inet->inet_rcv_saddr; __be32 saddr = inet->inet_daddr; int dif = sk->sk_bound_dev_if; /* 根据目的IP和源IP,生成一个64位的值 */ INET_ADDR_COOKIE(acookie, saddr, daddr); /* 根据目的端口和源端口,生成一个32位的值 */ const __portpair ports = INET_COMBINED_PORTS(inet->inet_dport, lport); struct net *net = sock_net(sk); /* 通过连接的四元组,计算得到一个哈希值 */ unsigned int hash = inet_ehashfn(net, daddr, lport, saddr, inet->inet_dport); /* 根据计算得到的哈希值,从哈希表中找到对应的哈希桶 */ struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash); /* 根据计算得到的哈希值,从哈希表中找到对应哈希桶的锁 */ spinlock_t *lock = inet_ehash_lockp(hinfo, hash); struct sock *sk2; const struct hlist_nulls_node *node; struct inet_timewait_sock *tw; int twrefcnt = 0; spin_lock(lock); /* 锁住哈希桶 */ /* Check TIME-WAIT sockets first. 遍历哈希桶 */ sk_nulls_for_each(sk2, node, &head->chain) { if (sk2->sk_hash != hash) /* 先比较哈希值,相同的才继续匹配 */ continue; /* 如果连接完全匹配:四元组相同、绑定的设备相同 */ if (likely(INET_MATCH(sk2, net, acookie, saddr, daddr, ports, dif))) { /* 此版本把ESTABLISHED和TIME_WAIT状态的连接放在同一个哈希桶中, * 所以需要判断连接状态是否为TIME_WAIT。 */ if (sk2->sk_state == TCP_TIME_WAIT) { tw = inet_twsk(sk2); /* 满足以下条件就允许复用: * 1. 使用TCP Timestamp选项。 * 2. 符合以下任一情况即可: * 2.1 twp == NULL,主动建立连接时,如果用户已经绑定端口了,那么会符合。 * 2.2 启用tcp_tw_reuse,且距离上次收到数据包的时间大于1s。 */ if (twsk_unique(sk, sk2, twp) break; } goto not_unique; } } /* 走到这里有两种情况: * 1. 遍历玩哈希桶,都没有找到四元组一样的。 * 2. 找到了四元组一样的,但是符合重用的条件。 */ /* Must record num and sport now. Otherwise we will see * in hash table socket with a funny identity. */ inet->inet_num = lport; /* 保存源端口 */ inet->inet_sport = htons(lport); sk->sk_hash = hash; /* 保存ehash表的哈希值 */ WARN_ON(! sk_unhashed(sk)); /* 要求新连接sk还没被链入ehash哈希表中 */ __sk_nulls_add_node_rcu(sk, &head->chain); /* 把此sk链入ehash哈希表中 */ /* tw不为空,说明已经找到一条完全匹配的、处于TIME_WAIT状态的连接, * 并且经过判断,此连接的端口可以复用。 */ if (tw) { twrefcnt = inet_twsk_unhash(tw); /* 把此twsk从ehash表中删除 */ NET_INC_STATS_BH(net, LINUX_MIB_TIMEWAITRECYCLED); } spin_unlock(lock); /* 释放哈希桶的锁 */ if (twrefcnt) /* 如果需要释放twsk */ inet_twsk_put(tw); /* 释放twsk实例 */ sock_prot_inuse_add(sock_net(sk), sk->s_prot, 1); /* 增加TCP协议的引用计数 */ /* 如果twp不为NULL,各种哈希表删除操作,就交给调用函数来处理 */ if (twp) { *twp = tw; } else if (tw) { /* 把tw从death_row、ehash、bhash的哈希表中删除,更新tw的引用计数 */ inet_twsk_deschedule(tw, death_row); inet_twsk_put(tw); /* 释放tw结构体 */ } return 0; not_unique: spin_unlock(lock); return -EADDRNOTAVAIL; }
端口初始偏移值
根据源IP、目的IP、目的端口,采用MD5计算出一个数值,即返回值offset。
static inline u32 inet_sk_port_offset (const struct sock *sk) { const struct inet_sock *inet = inet_sk(sk); return secure_ipv4_port_ephemeral(inet->inet_rcv_saddr, inet->inet_daddr, inet->inet_dport); } #define MD5_DIGEST_WORDS 4 #define MD5_MESSAGE_BYTES 64 #define NET_SECRET_SIZE (MD5_MESSAGE_BYTES / 4) static u32 net_secret[NET_SECRET_SIZE] ____cacheline_aligned; static __always_inline void net_secret_init(void) { net_get_random_once(net_secret, sizeof(net_secret)); /* 只取一次随机数 */ } u32 secure_ipv4_port_ephemeral(__be32 saddr, __be32 daddr, __be16 dport) { u32 hash[MD5_DIGEST_WORDS]; net_secret_init(); /* 随机生成MD5消息 */ hash[0] = (__force u32) saddr; hash[1] = (__force u32) daddr; hash[2] = (__force u32) dport ^ net_secret[14]; hash[3] = net_secret[15]; md5_transform(hash, net_secret); /* 计算MD5值,结果保存在hash数组中 */ return hash[0]; }