• reuseaddr和reuseport


    对于reuseaddr和reuseport的演进,可以参考这篇文章,本文主要基于kernel 3.18.79 版本分析下这两个参数如何生效。

    SO_REUSEADDR

    1. 解决server重启的问题
      server端调用close,client还没有调用close,则server端socket处于FIN_WAIT2状态,持续时间60s,此时server重启会失败,在bind时会报错 Address already in use。
      或者server端先调用close,client后调用close,则server会处于TIME_WAIT状态,持续时间也是60s,此时server重启会失败,在bind时也会报错 Address already in use。

    解决方法:
    套接字bind前设置SO_REUSEADDR,或者 SO_REUSEPORT

    1. 解决ip为零的通配符问题
      例如: socketA绑定了0.0.0.0:2222,socketB绑定10.164.129.22:2222时,或者 socketA绑定了10.164.129.22:2222,socketB绑定0.0.0.0:2222时,都会报错 Address already in use。因为0.0.0.0相当于通配符,可以匹配到10.164.129.22,在没有设置地址复用或者端口复用前就会有此问题。

    解决方法:
    方案1: 如果socketA调用bind后,又调用了listen,则fastreuse 会恢复为0(即使socketA在bind前设置了SO_REUSEADDR),此时即使socketB在bind前设置了SO_REUSEADDR也不管用。
    socketA和socketB在bind前设置SO_REUSEADDR, 并且socketB必须在socketA调用listen前调用bind。
    方案2: socketA和socketB在bind前均设置了SO_REUSEPORT。
    方案3: socketB在调用bind前设置 setsockopt(fd, SOL_TCP, TCP_REPAIR, &status, sizeof(int)) 进行强制bind,而不用管socketA是什么状态。

    SO_REUSEPORT
    SO_REUSEPORT支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:
    a. 允许多个套接字 bind()/listen() 同一个TCP/UDP端口
    每一个线程拥有自己的服务器套接字
    在服务器套接字上没有了锁的竞争
    b. 内核层面实现负载均衡
    c. 安全层面,监听同一个端口的套接字只能位于同一个用户下面

    如何设置
    可以在调用bind绑定端口号之前,通过如下调用设置socket的reuseaddr或者reuseport

    1. setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &status, sizeof(int))
    2. setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &status, sizeof(int))

    kernel中对应代码如下

    1. sock_setsockopt(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen)
    2. case SO_REUSEADDR:
    3. sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE);
    4. break;
    5. case SO_REUSEPORT:
    6. sk->sk_reuseport = valbool;

    出问题场景下(报错 Address already in use),代码分析
    a. socketA 调用bind时

    1. //在内核中会调用inet_bind,接着调用inet_csk_get_port,在函数
    2. //inet_csk_get_port中,先在bind hash表查找是否有其他socket绑
    3. //定此port了,第一次bind肯定查找失败,跳转到tb_not_found
    4. have_snum:
    5. //在bind hash表中查找,hash key为net和local port
    6. head = &hashinfo->bhash[inet_bhashfn(net, snum, hashinfo->bhash_size)];
    7. spin_lock(&head->lock);
    8. inet_bind_bucket_for_each(tb, &head->chain)
    9. if (net_eq(ib_net(tb), net) && tb->port == snum)
    10. goto tb_found;
    11. tb = NULL;
    12. goto tb_not_found;
    13. tb_not_found:
    14. ret = 1;
    15. //创建hash表项,并将表项添加到bind hash表中
    16. if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
    17. net, head, snum)) == NULL)
    18. goto fail_unlock;
    19. //第一次创建,并且是第一次调用,tb->owners为空
    20. if (hlist_empty(&tb->owners)) {
    21. //sk如果设置了SO_REUSEADDR,并且socketA不为TCP_LISTEN状态,
    22. //则设置tb->fastreuse为1
    23. if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
    24. tb->fastreuse = 1;
    25. else
    26. tb->fastreuse = 0;
    27. //sk如果设置了SO_REUSEPORT,则设置fastreuseport 为1,设置fastuid为uid
    28. if (sk->sk_reuseport) {
    29. tb->fastreuseport = 1;
    30. tb->fastuid = uid;
    31. } else
    32. tb->fastreuseport = 0;
    33. } else {
    34. //调用listen时,会再次调用inet_csk_get_port,此情况下才会走到此else分支。
    35. if (tb->fastreuse &&
    36. (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
    37. tb->fastreuse = 0;
    38. if (tb->fastreuseport &&
    39. (!sk->sk_reuseport || !uid_eq(tb->fastuid, uid)))
    40. tb->fastreuseport = 0;
    41. }
    42. success:
    43. //将tb赋给icsk_bind_hash
    44. if (!inet_csk(sk)->icsk_bind_hash)
    45. inet_bind_hash(sk, tb, snum);
    46. WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
    47. ret = 0;
    48. }

    b. socketA 调用listen时

    1. sk->sk_state = TCP_LISTEN;
    2. if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
    3. inet->inet_sport = htons(inet->inet_num);
    4. sk_dst_reset(sk);
    5. sk->sk_prot->hash(sk);
    6. return 0;
    7. }
    8. //在函数inet_csk_get_port中,先在bind hash表查找是否有其他
    9. //socket绑定此port了,这次会查找成功,跳转到tb_found
    10. have_snum:
    11. //在bind hash表中查找,hash key为net和local port
    12. head = &hashinfo->bhash[inet_bhashfn(net, snum, hashinfo->bhash_size)];
    13. spin_lock(&head->lock);
    14. inet_bind_bucket_for_each(tb, &head->chain)
    15. if (net_eq(ib_net(tb), net) && tb->port == snum)
    16. goto tb_found;
    17. tb = NULL;
    18. goto tb_not_found;
    19. tb_found:
    20. //不为空
    21. if (!hlist_empty(&tb->owners)) {
    22. //没有设置此标志
    23. if (sk->sk_reuse == SK_FORCE_REUSE)
    24. goto success;
    25. //fastreuse 和fastureseport都为0,走else分支
    26. if (((tb->fastreuse > 0 &&
    27. sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
    28. (tb->fastreuseport > 0 &&
    29. sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
    30. smallest_size == -1) {
    31. goto success;
    32. } else {
    33. ret = 1;
    34. //执行函数 inet_csk_bind_conflict,在调用listen情况下,会返回false,说明没有冲突的sk
    35. if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) {
    36. ....
    37. }
    38. }
    39. }
    40. tb_not_found:
    41. ret = 1;
    42. if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
    43. net, head, snum)) == NULL)
    44. goto fail_unlock;
    45. //tb->owners不为空,执行else分支
    46. if (hlist_empty(&tb->owners)) {
    47. //sk如果设置SO_REUSEADDR,并且socketA不为
    48. //TCP_LISTEN状态,则设置tb->fastreuse为1
    49. if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
    50. tb->fastreuse = 1;
    51. else
    52. tb->fastreuse = 0;
    53. //sk如果设置SO_REUSEPORT,则设置fastreuseport 为1,设置fastuid
    54. if (sk->sk_reuseport) {
    55. tb->fastreuseport = 1;
    56. tb->fastuid = uid;
    57. } else
    58. tb->fastreuseport = 0;
    59. } else {
    60. //调用listen时,会再次调用inet_csk_get_port,此情况下才会走到此else分支。
    61. //如果socketA调用bind后,又调用了listen,则fastreuse 会恢复为0(即使socketA在bind前设
    62. //置了SO_REUSEADDR)。此时即使socketB在bind前设置了SO_REUSEADDR也不管用。
    63. //但是fastreuseport 不会被恢复为0
    64. if (tb->fastreuse &&
    65. (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
    66. tb->fastreuse = 0;
    67. if (tb->fastreuseport &&
    68. (!sk->sk_reuseport || !uid_eq(tb->fastuid, uid)))
    69. tb->fastreuseport = 0;
    70. }
    71. success:
    72. //icsk_bind_hash已经有值
    73. if (!inet_csk(sk)->icsk_bind_hash)
    74. inet_bind_hash(sk, tb, snum);
    75. WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
    76. ret = 0;
    77. }
    78. int inet_csk_bind_conflict(const struct sock *sk,
    79. const struct inet_bind_bucket *tb, bool relax)
    80. {
    81. struct sock *sk2;
    82. int reuse = sk->sk_reuse;
    83. int reuseport = sk->sk_reuseport;
    84. kuid_t uid = sock_i_uid((struct sock *)sk);
    85. /*
    86. * Unlike other sk lookup places we do not check
    87. * for sk_net here, since _all_ the socks listed
    88. * in tb->owners list belong to the same net - the
    89. * one this bucket belongs to.
    90. */
    91. sk_for_each_bound(sk2, &tb->owners) {
    92. //因为是同一个sk,所以sk等于sk2,循环完后,sk2NULL
    93. if (sk != sk2 &&
    94. !inet_v6_ipv6only(sk2) &&
    95. (!sk->sk_bound_dev_if ||
    96. !sk2->sk_bound_dev_if ||
    97. sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
    98. if ((!reuse || !sk2->sk_reuse ||
    99. sk2->sk_state == TCP_LISTEN) &&
    100. (!reuseport || !sk2->sk_reuseport ||
    101. (sk2->sk_state != TCP_TIME_WAIT &&
    102. !uid_eq(uid, sock_i_uid(sk2))))) {
    103. if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr ||
    104. sk2->sk_rcv_saddr == sk->sk_rcv_saddr)
    105. break;
    106. }
    107. if (!relax && reuse && sk2->sk_reuse &&
    108. sk2->sk_state != TCP_LISTEN) {
    109. if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr ||
    110. sk2->sk_rcv_saddr == sk->sk_rcv_saddr)
    111. break;
    112. }
    113. }
    114. }
    115. return sk2 != NULL;
    116. }

    c. socketB 调用bind函数时,

    1. //在函数inet_csk_get_port中,先在bind hash表查找是否有其他
    2. //socket绑定此port了,这次会查找成功,跳转到tb_found
    3. have_snum:
    4. //在bind hash表中查找,hash key为net和local port
    5. head = &hashinfo->bhash[inet_bhashfn(net, snum,
    6. hashinfo->bhash_size)];
    7. spin_lock(&head->lock);
    8. inet_bind_bucket_for_each(tb, &head->chain)
    9. if (net_eq(ib_net(tb), net) && tb->port == snum)
    10. goto tb_found;
    11. tb = NULL;
    12. goto tb_not_found;
    13. tb_found:
    14. //tb->owners不为空
    15. if (!hlist_empty(&tb->owners)) {
    16. //没有设置此标志
    17. //如果socketB设置了TCP_REPAIR,则强制bind成功
    18. if (sk->sk_reuse == SK_FORCE_REUSE)
    19. goto success;
    20. //如果socketA没有设置reuseaddr和reuseport,则fastreuse 和fastureseport都为0,走else分支。
    21. //如果tb的fastreuse不为0,即socketA设置了reuseaddr,并且还没有调用listen,如果
    22. //socketB也设置了reuseaddr,则socketB可以bind成功。
    23. //或者tb的fastreuseport不为0,即socketA设置了reuseport,如果socketB也设置了reuseport,则socketB可以bind成功。
    24. if (((tb->fastreuse > 0 &&
    25. sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
    26. (tb->fastreuseport > 0 &&
    27. sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
    28. smallest_size == -1) {
    29. goto success;
    30. } else {
    31. ret = 1;
    32. //执行函数inet_csk_bind_conflict,在此例会返回true,说明找到了冲突的sk
    33. if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) {
    34. //不满足if条件,跳转到fail_unlock,返回值为1.
    35. //在inet_bind中,判断如果返回值为1,则设置err为EADDRINUSE
    36. if (((sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
    37. (tb->fastreuseport > 0 &&
    38. sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
    39. smallest_size != -1 && --attempts >= 0) {
    40. spin_unlock(&head->lock);
    41. goto again;
    42. }
    43. goto fail_unlock;
    44. }
    45. }
    46. }
    47. fail_unlock:
    48. spin_unlock(&head->lock);
    49. fail:
    50. local_bh_enable();
    51. return ret;
    52. }
    53. int inet_csk_bind_conflict(const struct sock *sk,
    54. const struct inet_bind_bucket *tb, bool relax)
    55. {
    56. struct sock *sk2;
    57. int reuse = sk->sk_reuse;
    58. int reuseport = sk->sk_reuseport;
    59. kuid_t uid = sock_i_uid((struct sock *)sk);
    60. /*
    61. * Unlike other sk lookup places we do not check
    62. * for sk_net here, since _all_ the socks listed
    63. * in tb->owners list belong to the same net - the
    64. * one this bucket belongs to.
    65. */
    66. sk_for_each_bound(sk2, &tb->owners) {
    67. if (sk != sk2 &&
    68. !inet_v6_ipv6only(sk2) &&
    69. (!sk->sk_bound_dev_if ||
    70. !sk2->sk_bound_dev_if ||
    71. sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
    72. if ((!reuse || !sk2->sk_reuse ||
    73. sk2->sk_state == TCP_LISTEN) &&
    74. (!reuseport || !sk2->sk_reuseport ||
    75. (sk2->sk_state != TCP_TIME_WAIT &&
    76. !uid_eq(uid, sock_i_uid(sk2))))) {
    77. //只要sk和sk2两个套接字绑定的ip地址,任意一个为全零(相当于通配符)或者
    78. //两个ip完全相等,就认为有地址冲突。
    79. if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr ||
    80. sk2->sk_rcv_saddr == sk->sk_rcv_saddr)
    81. break;
    82. }
    83. if (!relax && reuse && sk2->sk_reuse &&
    84. sk2->sk_state != TCP_LISTEN) {
    85. if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr ||
    86. sk2->sk_rcv_saddr == sk->sk_rcv_saddr)
    87. break;
    88. }
    89. }
    90. }
    91. return sk2 != NULL;
    92. }

    接收client连接
    如果有两个完全重复的套接字在监听,如下,哪个套接字接收客户端的请求呢?

    1. root@ubuntu:/home/jk/socket# netstat -nap | grep 2222
    2. tcp 0 0 192.168.122.1:2222 0.0.0.0:* LISTEN 46735/server1
    3. tcp 0 0 192.168.122.1:2222 0.0.0.0:* LISTEN 46728/server

    因为有多个相同的监听套接字,需要找个使用哪个套接字来处理。

    客户请求首先在tcp_v4_rcv中调用__inet_lookup查找,先调用__inet_lookup_established使用四元组在已建立连接表中查找,如果是第一次请求显然找不到,接着调用__inet_lookup_listener在监听表中查找,具体代码如下:

    1. struct sock *__inet_lookup_listener(struct net *net,
    2. struct inet_hashinfo *hashinfo,
    3. const __be32 saddr, __be16 sport,
    4. const __be32 daddr, const unsigned short hnum,
    5. const int dif)
    6. struct sock *sk, *result;
    7. struct hlist_nulls_node *node;
    8. unsigned int hash = inet_lhashfn(net, hnum);
    9. struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
    10. int score, hiscore, matches = 0, reuseport = 0;
    11. u32 phash = 0;
    12. result = NULL;
    13. hiscore = 0;
    14. sk_nulls_for_each_rcu(sk, node, &ilb->head) {
    15. //根据目的ip和目的端口号进行匹配。如果匹配成功则返回值大于0
    16. //返回值就是这次匹配得到的分数,最终分数高的胜出。匹配度越高分数越高。
    17. score = compute_score(sk, net, hnum, daddr, dif);
    18. if (score > hiscore) {
    19. result = sk;
    20. hiscore = score;
    21. //如果设置了reuseport选项
    22. reuseport = sk->sk_reuseport;
    23. if (reuseport) {
    24. phash = inet_ehashfn(net, daddr, hnum, saddr, sport);
    25. matches = 1;
    26. }
    27. } else if (score == hiscore && reuseport) {
    28. matches++;
    29. if (reciprocal_scale(phash, matches) == 0)
    30. result = sk;
    31. phash = next_pseudo_random32(phash);
    32. }
    33. }

    计算分数。
    三个匹配条件:sk_family ,socket绑定的本地ip rcv_saddr,socket绑定的本地接口sk_bound_dev_if。
    如果目的ip和socket绑定的ip地址不同或者报文入接口和socket绑定的接口不同,则得分为-1,
    说明匹配失败
    如果三个都匹配,则分数为10,为最高分数。

    1. static inline int compute_score(struct sock *sk, struct net *net,
    2. const unsigned short hnum, const __be32 daddr,
    3. const int dif)
    4. {
    5. int score = -1;
    6. struct inet_sock *inet = inet_sk(sk);
    7. //网络空间net必须相同,目的端口号必须和绑定的端口号相同,否则直接返回 -1
    8. if (net_eq(sock_net(sk), net) && inet->inet_num == hnum && !ipv6_only_sock(sk)) {
    9. __be32 rcv_saddr = inet->inet_rcv_saddr;
    10. //sk->sk_family为PF_INET 得分为2,否则为1
    11. score = sk->sk_family == PF_INET ? 2 : 1;
    12. if (rcv_saddr) {
    13. //目的ip和绑定ip不同,直接返回 -1
    14. if (rcv_saddr != daddr)
    15. return -1;
    16. //目的ip和绑定ip相同,得分加4
    17. score += 4;
    18. }
    19. //调用 SO_BINDTODEVICE 绑定了接口
    20. if (sk->sk_bound_dev_if) {
    21. //报文收接口和绑定接口不同,直接返回 -1
    22. if (sk->sk_bound_dev_if != dif)
    23. return -1;
    24. //报文收接口和绑定接口相同,得分加4
    25. score += 4;
    26. }
    27. }
    28. return score;
    29. }

    举例1:server上有三个监听套接字(前两个属于ip冲突的情况,需要设置reuseaddr才能bind成功),并且都没有绑定本地接口。
    0.0.0.0:80 --对应sock1
    192.168.1.2:80 --对应sock2
    10.24.35.142:80 --对应sock3

    如果此时client访问server: 192.168.1.2:80,则得分情况如下:
    匹配到0.0.0.0时,rcv_saddr为0,所以只能 score=2
    匹配到192.168.1.2时,rcv_saddr不为0,并且和请求目的ip相同,所以 socre=2+4=6
    匹配到10.24.35.142时,rcv_saddr不为0,但是和请求目的ip不同,所以score=-1
    所以最终返回的socket为最匹配的sock2

    举例2:server上有三个监听套接字(需要设置reuseport才能bind成功),并且都没有绑定本地接口。
    server端有三个监听socket
    192.168.1.2:80
    192.168.1.2:80
    192.168.1.2:80

    当有client连接到来时,用四元组计算hash,将结果对reuseport套接字数量取模,得到一个索引,该索引指示的数组位置对应的套接字便是工作套接字。

    同一条tcp流的前两个建立连接的请求syn和响应ack报文需要走上面流程,连接建立后,后续报文到来后,可直接在已建立连接表查找到。

    参考
    https://segmentfault.com/a/1190000020524323
    https://blog.csdn.net/dog250/article/details/51510823

    也可参考:https://www.jianshu.com/p/9cc2b5b9ad4d 

  • 相关阅读:
    pg 数据库,在新增的数据的时候,根据字段唯一性去更新数据
    用递归函数和栈操作逆序栈
    BERT 快速理解——思路简单描述
    2023年09月 Python(六级)真题解析#中国电子学会#全国青少年软件编程等级考试
    蓝桥杯(数论)练习
    稀疏表存储和查询
    游戏防沉迷系统相关内容
    HCIP之BGP路由反射器、联邦
    深度学习基础知识 Dataset 与 DataLoade的用法解析
    C++ ,VCPKG那些事
  • 原文地址:https://blog.csdn.net/fengcai_ke/article/details/126564040