一、前情回顧
上一節《socket位址綁定》中提到,應用程式傳遞過來的端口在核心中需要檢查端口是否可用:
if (sk->sk_prot->get_port(sk, snum)) {
inet->saddr = inet->rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
按照前面的例子來分析,這裡是調用了tcp_prot結構變量中的get_prot函數指針,該函數位于net/ipv4/Inet_connection_sock.c中;這個函數比較長,也是我們今天要分析的重點;
二、端口的管理
1、端口管理資料結構
Linux核心将所有socket使用時的端口通過一個哈希表來管理,該哈希表存放在全局變量tcp_hashinfo中,通過tcp_prot變量的h成員引用,該成員是一個聯合類型;對于tcp套接字類型,其引用存放在h. hashinfo成員中;下面是tcp_hashinfo的結構體類型:
struct inet_hashinfo {
struct inet_ehash_bucket *ehash;
rwlock_t *ehash_locks;
unsigned int ehash_size;
unsigned int ehash_locks_mask;
struct inet_bind_hashbucket *bhash;//管理端口的哈希表
unsigned int bhash_size;//端口哈希表的大小
struct hlist_head listening_hash[INET_LHTABLE_SIZE];
rwlock_t lhash_lock ____cacheline_aligned;
atomic_t lhash_users;
wait_queue_head_t lhash_wait;
struct kmem_cache *bind_bucket_cachep;//哈希表結構高速緩存
}
端口管理相關的,目前可以隻關注加注釋的這三個成員,其中bhash為已經哈希表結構,bhash_size為哈希表的大小;所有哈希表中的節點記憶體都是在bind_bucket_cachep高速緩存中配置設定;
下面看一下inet_bind_hashbucket結構體:
struct inet_bind_hashbucket {
spinlock_t lock;
struct hlist_head chain;
};
struct hlist_head {
struct hlist_node *first;
struct hlist_node {
struct hlist_node *next, **pprev;
inet_bind_hashbucket是哈希桶結構,lock成員是用于操作時對桶進行加鎖,chain成員是相同哈希值的節點的連結清單;示意圖如下:

2、預設端口的配置設定
當應用程式沒有指定端口時(如socket用戶端連接配接到服務端時,會由核心從可用端口中配置設定一個給該socket);
看看下面的代碼(參見net/ipv4/Inet_connection_sock.c: inet_csk_get_port()函數):
if (!snum) {
int remaining, rover, low, high;
inet_get_local_port_range(&low, &high);
remaining = (high - low) + 1;
rover = net_random() % remaining + low;
do {
head = &hashinfo->bhash[inet_bhashfn(rover, hashinfo->bhash_size)];
spin_lock(&head->lock);
inet_bind_bucket_for_each(tb, node, &head->chain)
if (tb->ib_net == net && tb->port == rover)
goto next;
break;
next:
spin_unlock(&head->lock);
if (++rover > high)
rover = low;
} while (--remaining > 0);
ret = 1;
if (remaining <= 0)
goto fail;
snum = rover;
這裡,随機端口的範圍是32768~61000;上面代碼的邏輯如下:
1) 從[32768, 61000]中随機取一個端口rover;
2) 計算該端口的hash值,然後從全局變量tcp_hashinfo的哈希表bhash中取出相同哈希值的連結清單head;
3) 周遊連結清單head,檢查每個節點的網絡裝置是否和目前網絡設定相同,同時檢查節點的端口是否和rover相同;
4) 如果相同,表明端口被占用,繼續下一個端口;如果和連結清單head中的節點都不相同,則跳出循環,繼續後面的邏輯;
inet_bind_bucket_foreach宏利用《建立socket》一文中提到的container_of宏來實作 的,大家可以自己看看;
3、端口重用
當應用程式指定端口時,參考下面的源代碼:
else {
head = &hashinfo->bhash[inet_bhashfn(snum, hashinfo->bhash_size)];
spin_lock(&head->lock);
inet_bind_bucket_for_each(tb, node, &head->chain)
if (tb->ib_net == net && tb->port == snum)
goto tb_found;
此時同樣會檢查該端口有沒有被占用;如果被占用,會檢查端口重用(跳轉到tb_found):
tb_found:
if (!hlist_empty(&tb->owners)) {
if (tb->fastreuse > 0 &&
sk->sk_reuse && sk->sk_state != TCP_LISTEN) {
goto success;
} else {
ret = 1;
if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb))
goto fail_unlock;
}
}
1) 端口節點結構
struct inet_bind_bucket {
struct net *ib_net;//端口所對應的網絡設定
unsigned short port;//端口号
signed short fastreuse;//是否可重用
struct hlist_node node;//作為bhash中chain連結清單的節點
struct hlist_head owners;//綁定在該端口上的socket連結清單
前面提到的哈希桶結構中的chain連結清單中的每個節點,其宿主結構體是inet_bind_bucket,該結構體通過成員node鍊傳入連結表;
2) 檢查端口是否可重用
這裡涉及到兩個屬性,一個是socket的sk_reuse,另一個是inet_bind_bucket的fastreuse;
sk_reuse可以通過setsockopt()庫函數進行設定,其值為0或1,當為1時,表示當一個socket進入TCP_TIME_WAIT狀态(連接配接關閉已經完成)後,它所占用的端口馬上能夠被重用,這在調試伺服器時比較有用,重新開機程式不用進行等待;而fastreuse代表該端口是否允許被重用:
l 當該端口第一次被使用時(owners為空),如果sk_reuse為1且socket狀态不為TCP_LISTEN,則設定fastreuse為1,否則設定為0;
l 當該端口同時被其他socket使用時(owners不為空),如果目前端口能被重用,但是目前socket的sk_reuse為0或其狀态為TCP_LISTEN,則将fastreuse設定為0,标記為不能重用;
3) 當不能重用時,再次檢查沖突
此時會調用inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb)再次檢查端口是否沖突;回想《建立socket》一文中提到,建立socket成功後,要使用相應的協定來初始化socket,對于tcp協定來說,其初始化方法是net/ipv4/Tcp_ipv4.c:tcp_v4_init_sock(),其中就做了如下一步的設定:
icsk->icsk_af_ops = &ipv4_specific;
struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.remember_stamp = tcp_v4_remember_stamp,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.bind_conflict = inet_csk_bind_conflict,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_ip_setsockopt,
.compat_getsockopt = compat_ip_getsockopt,
#endif
下面看看這裡再次檢查沖突的代碼:
int inet_csk_bind_conflict(const struct sock *sk,
const struct inet_bind_bucket *tb)
{
const __be32 sk_rcv_saddr = inet_rcv_saddr(sk);
struct sock *sk2;
struct hlist_node *node;
int reuse = sk->sk_reuse;
sk_for_each_bound(sk2, node, &tb->owners) {
if (sk != sk2 &&
!inet_v6_ipv6only(sk2) &&
(!sk->sk_bound_dev_if ||
!sk2->sk_bound_dev_if ||
sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
if (!reuse || !sk2->sk_reuse ||
sk2->sk_state == TCP_LISTEN) {
const __be32 sk2_rcv_saddr = inet_rcv_saddr(sk2);
if (!sk2_rcv_saddr || !sk_rcv_saddr ||
sk2_rcv_saddr == sk_rcv_saddr)
break;
}
return node != NULL;
上面函數的邏輯是:從owners中周遊綁定在該端口上的socket,如果某socket跟目前的socket不是同一個,并且是綁定在同一個網絡裝置接口上的,并且它們兩個之中至少有一個的sk_reuse表示自己的端口不能被重用或該socket已經是TCP_LISTEN狀态了,并且它們兩個之中至少有一個沒有指定接收IP位址,或者兩個都指定接收位址,但是接收位址是相同的,則沖突産生,否則不沖突。
也就是說,不使用同一個接收位址的socket可以共用端口号,綁定在不同的網絡裝置接口上的socket可以共用端口号,或者兩個socket都表示自己可以被重用,并且還不在TCP_LISTEN狀态,則可以重用端口号。
4、建立inet_bind_bucket
當在bhash中沒有找到指定的端口時,需要建立新的桶節點,然後挂入bhash中:
tb_not_found:
ret = 1;
if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
net, head, snum)) == NULL)
goto fail_unlock;
if (hlist_empty(&tb->owners)) {
if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
tb->fastreuse = 1;
else
tb->fastreuse = 0;
} else if (tb->fastreuse &&
(!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
tb->fastreuse = 0;
success:
if (!inet_csk(sk)->icsk_bind_hash)
inet_bind_hash(sk, tb, snum);
有興趣的可以自己看看這段代碼的實作,這裡就不再展開了。