• (2) linux 3.x



    http://blog.csdn.net/zhangskd/article/details/47374373


    主要内容: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. 内存不够,无法创建端口的存储结构。

    1. /* Bind a port for a connect operation and hash it. */  
    2. int inet_hash_connect (struct inet_timewait_death_row *death_row, struct sock *sk)  
    3. {  
    4.     return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk),   
    5.         __inet_check_established, __inet_hash_nolisten);  
    6. }  

    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哈希表。

    1. int __inet_hash_connect (struct inet_timewait_death_row *death_row,   
    2.     struct sock *sk, u32 port_offset,  
    3.     int (*check_established)(struct inet_timewait_death_row *, struct sock *,  
    4.          __u16, struct inet_timewait_sock **),  
    5.     int (*hash)(struct sock *sk, struct inet_timewait_sock *twp))  
    6. {  
    7.     struct inet_hashinfo *hinfo = death_row->hashinfo; /* tcp_hashinfo */  
    8.     const unsigned short snum = inet_sk(sk)->inet_num; /* 本端端口 */  
    9.     struct inet_bind_hashbucket *head;  
    10.     struct inet_bind_bucket *tb;  
    11.     int ret;  
    12.     struct net *net = sock_net(sk);  
    13.     int twrefcnt = 1;  
    14.   
    15.     /* snum为0时,表示用户没有绑定端口,默认让系统自动选取端口 */  
    16.     if (! snum) {  
    17.         int i, remaining, low, high, port;  
    18.         static u32 hint; /* 用于保存上次查找的位置 */  
    19.         u32 offset = hint + port_offset;  
    20.         struct inet_timewait_sock *tw = NULL;  
    21.           
    22.         /* 系统自动分配时,获取端口号的取值范围 */  
    23.         inet_get_local_port_range(net, &low, &high);  
    24.         remaining = (high - low) + 1/* 取值范围内端口号的个数 */  
    25.    
    26.         local_bh_disable();  
    27.         for (i = 1; i <= remaining; i++) {  
    28.             /* 根据MD5计算得到的port_offset值,以及hint,获取范围内的一个端口 */  
    29.             port = low + (i + offset) % remaining;   
    30.    
    31.             /* 如果此端口号属于保留的,那么直接跳过 */  
    32.             if (inet_is_reserved_local_port(port))  
    33.                 continue;  
    34.   
    35.             /* 根据端口号,找到所在的哈希桶 */  
    36.             head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)];  
    37.             spin_lock(&head->lock); /* 锁住此哈希桶 */  
    38.   
    39.             /* 从头遍历哈希桶 */  
    40.             inet_bind_bucket_for_each(tb, &head->chain) {  
    41.                 /* 如果此端口已经被使用了 */  
    42.                 if (net_eq(ib_net(tb), net) && tb->port == port) {  
    43.   
    44.                     /* 不允许使用已经被bind()绑定的端口,无论此端口是否能够被复用 */  
    45.                     if (tb->fastreuse >= 0 || tb->fastreuseport >= 0)  
    46.                         goto next_port;  
    47.   
    48.                     WARN_ON(hlist_empty(&tb->owners));  
    49.   
    50.                    /* 检查端口是否允许重用 */  
    51.                     if (! check_established(death_row, sk, port, &tw))  
    52.                         goto ok; /* 成功,该端口可以被重复使用 */  
    53.                     goto next_port; /* 失败 */  
    54.                 }  
    55.             }  
    56.    
    57.             /* 走到这里,表示该端口尚未被使用。 
    58.              * 创建一个inet_bind_bucket实例,并把它加入到哈希桶中。 
    59.              */  
    60.             tb = inet_bind_bucket_create(hinfo->bind_bucket, cachep, net, head, port);  
    61.   
    62.             /* 如果内存不够,则退出端口选择。 
    63.              * 会导致connect()失败,返回-EADDRNOTAVAIL。 
    64.              */  
    65.             if (! tb) {  
    66.                 spin_unlock(&head->lock);  
    67.                 break;  
    68.             }  
    69.    
    70.             tb->fastreuse = -1;  
    71.             tb->fastreuseport = -1;  
    72.             goto ok;  
    73.    
    74.             next_port:  
    75.                 spin_unlock(&head->lock);  
    76.         } /* end of for */  
    77.   
    78.         local_bh_enable();  
    79.    
    80.         /* 有两种可能:内存不够、端口区间内的端口号用光 */  
    81.         return -EADDRNOTAVAIL; /* Cannot assign requested address */  
    82.   
    83.  ok:  
    84.         hint += i; /* 下一次connect()时,查找端口增加了这段偏移 */  
    85.    
    86.         /* Head lock still held and bh's disabled. 
    87.          * 把tb赋值给icsk->icsk_bind_hash,更新inet->inet_num,把sock链入tb->owners哈希链中。 
    88.          * 更新该端口的绑定次数,系统总的端口绑定次数。 
    89.          */  
    90.         inet_bind_hash(sk, tb, port);  
    91.    
    92.         /* 如果sk尚未链入ehash哈希表中 */  
    93.         if (sk_unhashed(sk)) {  
    94.             inet_sk(sk)->inet_sport = htons(port); /* 保存本地端口 */  
    95.             twrefcnt += hash(sk, tw); /* 把sk链入到ehash哈希表中,把tw从ehash表中删除 */  
    96.         }  
    97.   
    98.         if (tw)  
    99.             twrefcnt += inet_twsk_bind_unhash(tw, hinfo); /* 把tw从该端口的使用者链表中删除 */  
    100.   
    101.         spin_unlock(&head->lock);  
    102.   
    103.         if (tw) {  
    104.             /* 把tw从tcp_death_row、ehash、bhash的哈希表中删除,更新tw的引用计数 */  
    105.             inet_twsk_deschedule(tw, death_row);  
    106.   
    107.             while (twrefcnt) {  
    108.                 twrefcnt--;  
    109.                 inet_twsk_put(tw); /* 释放tw结构体 */  
    110.             }  
    111.         }  
    112.    
    113.         ret = 0;  
    114.         goto out;  
    115.     }  
    116.       
    117.     /* 走到这里,表示用户已经自己绑定了端口 */  
    118.     head = &hinfo->bhash[inet_bhashfn(net, snum, hinfo->bhash_size)]; /* 端口所在的哈希桶 */  
    119.     tb = inet_csk(sk)->icsk_bind_hash; /* 端口的存储实例 */  
    120.   
    121.     spin_lock_bh(&head->lock);  
    122.   
    123.     /* 如果sk是此端口的使用者队列的第一个节点 */  
    124.     if (sk_head(&tb->owners) == sk && ! sk->sk_bind_node.next) {  
    125.         hash(sk, NULL); /* 计算sk在ehash中的索引,赋值给sk->sk_hash,把sk链入到ehash表中 */  
    126.         spin_unlock_bh(&head->lock);  
    127.         return 0;  
    128.   
    129.     } else {  
    130.         spin_unlock(&head->lock);  
    131.         /* No definite answer... Walk to established hash table */  
    132.         ret = check_established(death_row, sk, snum, NULL); /* 查看是否有可以重用的端口 */  
    133.   
    134. out:  
    135.   
    136.         local_bh_enable();  
    137.         return ret;  
    138.     }  
    139. }  

    根据四元组,计算sk在ehash哈希表中的索引,保存到sk->sk_hash中,然后把sk链入ehash哈希表。

    1. int __inet_hash_nolisten(struct sock *sk, struct inet_timewait_sock *tw)  
    2. {  
    3.     struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;  
    4.     struct hlist_nulls_head *list;  
    5.     spinlock_t *lock;  
    6.     struct inet_ehash_bucket *head;  
    7.     int twrefcnt = 0;   
    8.   
    9.     WARN_ON(! sk_unhashed(sk));  
    10.   
    11.     sk->sk_hash = inet_sk_ehashfn(sk); /* 根据四元组,计算在ehash哈希表中的索引 */  
    12.     head = inet_ehash_bucket(hashinfo, sk->sk_hash); /* 根据索引,找到对应的哈希桶 */  
    13.     list = &head->chain;  
    14.     lock = inet_ehash_lockp(hashinfo, sk->sk_hash); /* 根据索引,找到对应哈希桶的锁 */  
    15.   
    16.     spin_lock(lock);  
    17.     __sk_nulls_add_node_rcu(sk, list); /* 把sk->sk_null_node链入链表 */   
    18.   
    19.     if (tw) { /* 如果复用了TIME_WAIT sock的端口 */  
    20.         WARN_ON(sk->sk_hash != tw->tw_hash);  
    21.   
    22.         /* 把tw从ehash表中删除,返回值如果为1,表示释放锁之后,需要调用inet_twsk_put() */  
    23.         twrefcnt = inet_twsk_unhash(tw);   
    24.     }  
    25.   
    26.     spin_unlock(lock);  
    27.     sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1); /* 增加TCP协议的引用计数 */  
    28.   
    29.     return twrefcnt;  
    30. }  

    通过源IP、目的IP、源端口、目的端口,计算得到一个32位的哈希值。

    赋值给sk->sk_hash,作为索引,用于定位ehash中的哈希桶。

    1. static unsigned int inet_sk_ehashfn(const struct sock *sk)  
    2. {  
    3.     const struct inet_sock *inet = inet_sk(sk);  
    4.     const __be32 laddr = inet->inet_rcv_saddr;  
    5.     const __u16 lport = inet->inet_num;  
    6.     const __be32 faddr = inet->inet_daddr;  
    7.     const __be16 fport = inet->inet_dport;  
    8.     struct net *net = sock_net(sk);  
    9.   
    10.     return inet_ehashfn(net, laddr, lport, faddr, fport);  
    11. }  
    12.   
    13. static inline unsigned int inet_ehashfn (struct net *net, const __be32 laddr, const __u16 lport,  
    14.     const __be32 faddr, const __be16 fport)  
    15. {  
    16.     return jhash_3words((__force __u32) laddr, (__force __u32) faddr,  
    17.         ((__u32) lport) << 16 | (__force __u32) fport,  
    18.         inet_ehash_secret + net_hash_mix(net));  
    19. }   
    20.    
    21. u32 inet_ehash_secret __read_mostly;  
    22.   
    23. /* inet_ehash_secret must be set exactly once */  
    24. void build_ehash_secret(void)  
    25. {  
    26.     u32 rnd;  
    27.     do {  
    28.         get_random_bytes(&rnd, sizeof(rnd));  
    29.     } while (rnd == 0);  
    30.    
    31.     /* cmpxchg(void *ptr, unsigned long old, unsigned long new) 
    32.      * 比较*ptr和old: 
    33.      * 如果相等,则将new写入*ptr,返回old。 
    34.      * 如果不相等,返回*ptr。 
    35.      * 这里用于确保inet_ehash_secret只被写入一次。 
    36.      */  
    37.     cmpxchg(&inet_ehash_secret, 0, rnd);  
    38. }   

    端口重用

    __inet_check_established()用来检查已经在使用中的端口是否可以重用。

    如果在ehash哈希表中没有找到一条四元组相同的连接,这个端口当然允许重用。

    如果在ehash哈希表中找到一条完全一样的连接,即四元组相同、绑定的设备相同,

    那么还要符合以下条件:

    1. 连接的状态为TCP_TIME_WAIT。

    2. 使用了TCP_TIMESTAMP选项。

    3. 使用tcp_tw_reuse,并且此连接最近收到数据包的时间在1s以前。

    1. /* called with local bh disabled */  
    2. static int __inet_check_established(struct inet_timewait_death_row *death_row,  
    3.         struct sock *sk, __u16 lport, struct inet_timewait_sock **twp)  
    4. {  
    5.     struct inet_hashinfo *hinfo = death_row->hashinfo;  
    6.     struct inet_sock *inet = inet_sk(sk);  
    7.     __be32 daddr = inet->inet_rcv_saddr;  
    8.     __be32 saddr = inet->inet_daddr;  
    9.     int dif = sk->sk_bound_dev_if;  
    10.     /* 根据目的IP和源IP,生成一个64位的值 */  
    11.     INET_ADDR_COOKIE(acookie, saddr, daddr);  
    12.     /* 根据目的端口和源端口,生成一个32位的值 */  
    13.     const __portpair ports = INET_COMBINED_PORTS(inet->inet_dport, lport);  
    14.     struct net *net = sock_net(sk);  
    15.     /* 通过连接的四元组,计算得到一个哈希值 */  
    16.     unsigned int hash = inet_ehashfn(net, daddr, lport, saddr, inet->inet_dport);  
    17.     /* 根据计算得到的哈希值,从哈希表中找到对应的哈希桶 */  
    18.     struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);  
    19.     /* 根据计算得到的哈希值,从哈希表中找到对应哈希桶的锁 */  
    20.     spinlock_t *lock = inet_ehash_lockp(hinfo, hash);  
    21.     struct sock *sk2;  
    22.     const struct hlist_nulls_node *node;  
    23.     struct inet_timewait_sock *tw;  
    24.     int twrefcnt = 0;  
    25.   
    26.     spin_lock(lock); /* 锁住哈希桶 */  
    27.   
    28.     /* Check TIME-WAIT sockets first. 遍历哈希桶 */  
    29.     sk_nulls_for_each(sk2, node, &head->chain) {  
    30.         if (sk2->sk_hash != hash) /* 先比较哈希值,相同的才继续匹配 */  
    31.             continue;  
    32.    
    33.         /* 如果连接完全匹配:四元组相同、绑定的设备相同 */  
    34.         if (likely(INET_MATCH(sk2, net, acookie, saddr, daddr, ports, dif))) {  
    35.   
    36.             /* 此版本把ESTABLISHED和TIME_WAIT状态的连接放在同一个哈希桶中, 
    37.              * 所以需要判断连接状态是否为TIME_WAIT。 
    38.              */  
    39.             if (sk2->sk_state == TCP_TIME_WAIT) {  
    40.                 tw = inet_twsk(sk2);  
    41.   
    42.                 /* 满足以下条件就允许复用: 
    43.                  * 1. 使用TCP Timestamp选项。 
    44.                  * 2. 符合以下任一情况即可: 
    45.                  *     2.1 twp == NULL,主动建立连接时,如果用户已经绑定端口了,那么会符合。 
    46.                  *     2.2 启用tcp_tw_reuse,且距离上次收到数据包的时间大于1s。 
    47.                  */  
    48.                 if (twsk_unique(sk, sk2, twp)  
    49.                     break;  
    50.             }  
    51.   
    52.             goto not_unique;  
    53.         }  
    54.     }  
    55.   
    56.     /* 走到这里有两种情况: 
    57.      * 1. 遍历玩哈希桶,都没有找到四元组一样的。 
    58.      * 2. 找到了四元组一样的,但是符合重用的条件。 
    59.      */  
    60.    
    61.     /* Must record num and sport now. Otherwise we will see 
    62.      * in hash table socket with a funny identity. 
    63.      */  
    64.     inet->inet_num = lport; /* 保存源端口 */  
    65.     inet->inet_sport = htons(lport);  
    66.     sk->sk_hash = hash; /* 保存ehash表的哈希值 */  
    67.   
    68.     WARN_ON(! sk_unhashed(sk)); /* 要求新连接sk还没被链入ehash哈希表中 */  
    69.     __sk_nulls_add_node_rcu(sk, &head->chain); /* 把此sk链入ehash哈希表中 */  
    70.    
    71.    /* tw不为空,说明已经找到一条完全匹配的、处于TIME_WAIT状态的连接, 
    72.     * 并且经过判断,此连接的端口可以复用。 
    73.     */  
    74.     if (tw) {  
    75.         twrefcnt = inet_twsk_unhash(tw); /* 把此twsk从ehash表中删除 */  
    76.         NET_INC_STATS_BH(net, LINUX_MIB_TIMEWAITRECYCLED);  
    77.     }  
    78.   
    79.     spin_unlock(lock); /* 释放哈希桶的锁 */  
    80.   
    81.     if (twrefcnt) /* 如果需要释放twsk */  
    82.         inet_twsk_put(tw); /* 释放twsk实例 */  
    83.   
    84.     sock_prot_inuse_add(sock_net(sk), sk->s_prot, 1); /* 增加TCP协议的引用计数 */  
    85.    
    86.     /* 如果twp不为NULL,各种哈希表删除操作,就交给调用函数来处理 */  
    87.     if (twp) {  
    88.         *twp = tw;  
    89.     } else if (tw) {  
    90.         /* 把tw从death_row、ehash、bhash的哈希表中删除,更新tw的引用计数 */  
    91.         inet_twsk_deschedule(tw, death_row);  
    92.         inet_twsk_put(tw); /* 释放tw结构体 */  
    93.     }  
    94.   
    95.     return 0;  
    96.   
    97. not_unique:  
    98.     spin_unlock(lock);  
    99.     return -EADDRNOTAVAIL;  
    100. }    

    端口初始偏移值

    根据源IP、目的IP、目的端口,采用MD5计算出一个数值,即返回值offset。

    1. static inline u32 inet_sk_port_offset (const struct sock *sk)  
    2. {  
    3.     const struct inet_sock *inet = inet_sk(sk);  
    4.     return secure_ipv4_port_ephemeral(inet->inet_rcv_saddr, inet->inet_daddr, inet->inet_dport);  
    5. }  
    6.   
    7. #define MD5_DIGEST_WORDS 4  
    8. #define MD5_MESSAGE_BYTES 64  
    9. #define NET_SECRET_SIZE (MD5_MESSAGE_BYTES / 4)  
    10. static u32 net_secret[NET_SECRET_SIZE] ____cacheline_aligned;  
    11.   
    12. static __always_inline void net_secret_init(void)  
    13. {  
    14.     net_get_random_once(net_secret, sizeof(net_secret)); /* 只取一次随机数 */  
    15. }  
    16.   
    17. u32 secure_ipv4_port_ephemeral(__be32 saddr, __be32 daddr, __be16 dport)  
    18. {  
    19.     u32 hash[MD5_DIGEST_WORDS];  
    20.     net_secret_init(); /* 随机生成MD5消息 */  
    21.   
    22.     hash[0] = (__force u32) saddr;  
    23.     hash[1] = (__force u32) daddr;  
    24.     hash[2] = (__force u32) dport ^ net_secret[14];  
    25.     hash[3] = net_secret[15];  
    26.   
    27.     md5_transform(hash, net_secret); /* 计算MD5值,结果保存在hash数组中 */  
    28.   
    29.     return hash[0];  
    30. }  


  • 相关阅读:
    Leetcode Palindrome Linked List
    Leetcode Delete Node in a Linked List
    Leetcode Ugly Number
    Python 列表解析
    Python 生成器以及应用
    Python 迭代器协议以及可迭代对象、迭代器对象
    Python 装饰器
    Python 函数的嵌套
    Python 闭包函数
    Python 名称空间与作用域
  • 原文地址:https://www.cnblogs.com/ztguang/p/12644869.html
Copyright © 2020-2023  润新知