TCP选项分析 之 SO_REUSEADDR

TCP选项分析 之 SO_REUSEADDR

首先 从工程角度考虑如下问题

问题:
1:Server 进行 bind(),listen(),accept(),然后进行等待Client连接。
2:Client 进行 连接。即,Server的accept()返回client_fd
3:Server 程序退出(异常退出,或者)。但是和客户端的连接依旧存在。
4:Server 重启程序,但是此时Server进行bind()失败,提示端口被占用。

解决方法:
在1时,Server accept之前对listen_fd进行SO_REUSEADDR设置,或者在2时,对client_fd进行SO_REUSEADDR

常见误区1:
通常网上的文章告诉我们都是需要对Server的listen_fd进行SO_REUSEADDR设置,而忽略了其实际也可对其所有client_fd进行SO_REUSEADDR设置。

常见误区2:
SO_REUSEADDR不仅仅只忽略time_wait状态的socket,是忽略所有的带SO_REUSEADDR的任意状态的socket。例如,如果存在状态为establish状态的socket,只要这个socket设置了SO_REUSEADDR,第二次Server照样能起来。

常见误区3:
即使设置了SO_REUSEADDR,也不能同时建立2个独立进程listen同一个端口。想要此功能,请看SO_REUSEPORT分析,或者父进程listen然后fork子进程,这样父子进程都能同时accept。

常见误区4:
即使绑定的端口一样,也不一定被内核认为端口冲突,还有其他条件,后文会讲。

常见误区5:
SO_REUSEADDR对客户端是不生效的。例如Client bind了一个已经存在的socket一样的本地端口,然后connect到的server地址也是这个已经存在的的socket的Server地址,即五元组冲突,所以无论如何都不可能通过SO_REUSEADDR进行强行bind

内核源码分析1

首先,看SO_REUSEADDR如何设置的:

内核函数 sock_setsockopt有这么一段代码:

    case SO_REUSEADDR:
        sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE);
        break;

如果设置SO_REUSEADDR为1,那么 sk->sk_reuse 值为 1,反之为0。

内核源码分析2

常见误区可以猜到,因为对每个client_fd进行SO_REUSEADDR设置,和只对listen_fd设置效果是一样的,那么必然能猜想到,client_fdSO_REUSEADDR属性是继承自listen_fd的。

首先,关于TCP被动打开源码分析,见我的老博文 :https://blog.csdn.net/mrpre/article/details/24670659
这里,直接看accept()阻塞时是如何创建client_fd对应的sock的以及client_fd是如何继承listen_fdSO_REUSEADDR属性的。

三次握手完成时,即受到客户端的ACK时,处理流程的调用栈如下:
tcp_v4_syn_recv_sock->tcp_create_openreq_child->inet_csk_clone_lock

struct sock *tcp_create_openreq_child(struct sock *sk, struct request_sock *req, struct sk_buff *skb)
{
    struct sock *newsk = inet_csk_clone_lock(sk, req, GFP_ATOMIC);
    ......
}

上面,入参sk是listen_fd对应的sk,newsk是client_fd对应的sk。从 函数名字inet_csk_clone_lock中的clone可以推测数,继承的操作就在该函数中。

struct sock *inet_csk_clone_lock(const struct sock *sk,
                 const struct request_sock *req,
                 const gfp_t priority)
{
    struct sock *newsk = sk_clone_lock(sk, priority);//核心函数
    ......
}

struct sock *sk_clone_lock(const struct sock *sk, const gfp_t priority)
{
    struct sock *newsk;

    newsk = sk_prot_alloc(sk->sk_prot, priority, sk->sk_family);
    if (newsk != NULL) {
        struct sk_filter *filter;

        sock_copy(newsk, sk);//核心函数
        ....
    }
}

static void sock_copy(struct sock *nsk, const struct sock *osk)
{
    memcpy(nsk, osk, offsetof(struct sock, sk_dontcopy_begin));

    memcpy(&nsk->sk_dontcopy_end, &osk->sk_dontcopy_end,
           osk->sk_prot->obj_size - offsetof(struct sock, sk_dontcopy_end));
}

sock_copy函数拷贝 osk的数据到nsk中,具体拷贝的内容,是osk起始地址到&osk->sk_dontcopy_end,而osk->sk_reuse恰好就在该范围内,nsk自然就继承了sk_reuse属性。

内核源码分析3

TCP的bind失败核心代码
sys_bind->inet_bind->inet_csk_get_port
说明:linux内核用struct inet_bind_bucket描述一个被绑定的端口,加到全局的表中。
只要有人创建一个新的端口,就创建一个struct inet_bind_bucket对象。

int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
    struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
    struct inet_bind_hashbucket *head;
    struct inet_bind_bucket *tb;
    int ret, attempts = 5;
    struct net *net = sock_net(sk);
    int smallest_size = -1, smallest_rover;
    kuid_t uid = sock_i_uid(sk);

    local_bh_disable();
    if (!snum) {
        //没有指定端口,显然对于调用bind函数的Server来说这里直接忽略

    } else {
have_snum:
        //根据bind的端口:snum算hash值,取hash桶的index,head为冲突链表的表头
        head = &hashinfo->bhash[inet_bhashfn(net, snum,
                hashinfo->bhash_size)];
        spin_lock(&head->lock);
        inet_bind_bucket_for_each(tb, &head->chain)
            //冲突链表中有,说明有socket绑定了端口
            if (net_eq(ib_net(tb), net) && tb->port == snum)
                goto tb_found;
    }
    tb = NULL;
    goto tb_not_found;
tb_found:
    if (!hlist_empty(&tb->owners)) {
        //说明这个有人端口现在有socket绑定了
        if (sk->sk_reuse == SK_FORCE_REUSE)
            goto success;

        if (((tb->fastreuse > 0 &&
              sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
             (tb->fastreuseport > 0 &&
              sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
            smallest_size == -1) {
            goto success;
        } else {
            ret = 1;
            //检查是否冲突,端口一样,但是绑定的接口不一样,不能算冲突,下文细说。
            if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) {
                if (((sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
                     (tb->fastreuseport > 0 &&
                      sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
                    smallest_size != -1 && --attempts >= 0) {
                    spin_unlock(&head->lock);
                    goto again;
                }

                goto fail_unlock;
            }
        }
    }
tb_not_found:
    //到这里说明端口没有冲突。
    ret = 1;

    //tb为null说明压根没有socket绑定过该端口;tb不为null,说明有其他socket绑定,但是经过判断不认为冲突。
    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;
        if (sk->sk_reuseport) {
            tb->fastreuseport = 1;
            tb->fastuid = uid;
        } else
            tb->fastreuseport = 0;
    } else {
        if (tb->fastreuse &&
            (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
            tb->fastreuse = 0;
        if (tb->fastreuseport &&
            (!sk->sk_reuseport || !uid_eq(tb->fastuid, uid)))
            tb->fastreuseport = 0;
    }
success:
    if (!inet_csk(sk)->icsk_bind_hash)
        inet_bind_hash(sk, tb, snum);//所有绑定到tb的sock,都需要挂到tb->owners上去
    WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
    ret = 0;

fail_unlock:
    spin_unlock(&head->lock);
fail:
    local_bh_enable();
    return ret;
}

当我们Server创建一个socket,并且进行bind()的时候,就创建一个tb,且把tb插入到hash表中,且把这个listen_fd对于的sock挂到tb->owners中。其次,当TCP请求过来时,调用tcp_v4_syn_recv_sock->__inet_inherit_port->inet_bind_hashclient_fd对于的sock也挂到了tb->owners。

当Server进程挂了或者人肉被kill掉之后,tb->owners还剩下client_fd的sock。此时,如果我们启动另一个进程进行bind的时候,还会执行到inet_csk_get_port,遍历tb的冲突链,会执行goto tb_found,执行inet_csk(sk)->icsk_af_ops->bind_conflict即执行inet_csk_bind_conflict函数。

这里不列出inet_csk_bind_conflict具体源码,直接说明里面判断的逻辑:

  1. 如果当前待绑定的的sock和其余tb->owners中的sock不是绑定同一个接口,则不进行比较,则不认为是不冲突。

  2. 如果当前待绑定的的sock和其余tb->owners中的sock绑定的是同一个接口,但是ip地址不一样,则不认为是冲突。

  3. 如果当前待绑定的的sock和其余tb->owners中的sock是同一个接口且同一个ip,如果其余的sock是listen的sock,则必须当前sock和其余socket都需要开启reuseport。

  4. 如果当前sock和其余sock都开启了SO_REUSEADDR,且其余sock不是listen的,则不认为是冲突,换几句话说,如果每次建立的socket都设置了SO_REUSEADDR,且只进行了bind操作,其余的sock都没有进行listen
    (当前的sock可以是listen),则不认为冲突。这个条件就是本文最开始提到的问题的解决方案。

其余和reuseport相关的不在这里解释。

后记:

在实验的过程中发现,为何两个独立的进程只要设置了SO_REUSEADDR就允许bind同一个地址?有何意义?有何功能呢?其实是自己多虑了过分关注的bind()操作。作为一个Client,进行connect即使不进行bind也会随机分配一个端口。所以所谓2次的bind,其中一个bind理解为有人占用端口就行了。

你可能感兴趣的:(Socket源码分析,Socket选项)