分类
linux network

TCP socket connect

TCP 在作为客户端时,创建完socket一般都会立即向服务端发起connect请求。如果用户没有事先调用bind 绑定源端口和源ip。则内核会选取一个源端口,并根据路由选择源IP。
TCP connect 的大致流程是,调用inet的connect函数,调用TCP的connect函数,在TCP的connect函数中,设置socket状态。发送syn报文,设置超时重发,将socket 添加到establish hash 表中,阻塞等待服务端发送syn/ack报文。等到syn/ack报文,回ack报文,并返回,或超时返回。
系统调用入口函数 __sys_connect 如下

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
        struct socket *sock;
        struct sockaddr_storage address;
        int err, fput_needed;

        sock = sockfd_lookup_light(fd, &err, &fput_needed); //通过fd获取sock结构
        if (!sock)
                goto out;
        err = move_addr_to_kernel(uservaddr, addrlen, &address); //拷贝服务端地址
        if (err < 0)
                goto out_put;

        err =
            security_socket_connect(sock, (struct sockaddr *)&address, addrlen); //感觉像是selinux的hook。没仔细研究过,这块好多东西
        if (err)
                goto out_put;

        err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
                                 sock->file->f_flags); //调用 inet_stream_ops  inet_stream_connect。进行connect
out_put:
        fput_light(sock->file, fput_needed);
out:
        return err;
}

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
                int, addrlen)
{
        return __sys_connect(fd, uservaddr, addrlen);
}

最后调用 __inet_stream_connect 这个函数。connect最后阻塞在这个函数里面。函数重要代码如下

int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
                          int addr_len, int flags, int is_sendmsg)
{
    ......
            case SS_UNCONNECTED:
                err = -EISCONN;
                if (sk->sk_state != TCP_CLOSE)
                        goto out;

                if (BPF_CGROUP_PRE_CONNECT_ENABLED(sk)) { //连接前的 bpf操作,进去看了一下 如果没有bpf prog 很快出来
                        err = sk->sk_prot->pre_connect(sk, uaddr, addr_len);
                        if (err)
                                goto out;
                }

                err = sk->sk_prot->connect(sk, uaddr, addr_len); //执行具体协议的connect函数
                if (err < 0)
                        goto out;

                sock->state = SS_CONNECTING; //修改socket状态为connecting

                if (!err && inet_sk(sk)->defer_connect)
                        goto out;

                /* Just entered SS_CONNECTING state; the only
                 * difference is that return value in non-blocking
                 * case is EINPROGRESS, rather than EALREADY.
                 */
                err = -EINPROGRESS;
                break;
        }

        timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); //获取超时时间,默认非常大。

        if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
                int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
                                tcp_sk(sk)->fastopen_req &&
                                tcp_sk(sk)->fastopen_req->data ? 1 : 0;

                /* Error code is set above */
                if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
                    /*等待连接,连接超时(这个超时是TCP设置的)或者收到syn/ack。
                    *如果没有设置O_NOBLOCK则进程被阻塞。否则直接返回,直接返回后通过
                    *epoll等待连接事件。在某些版本的内核里面超时以后,这块行为会变成可读
                    *可写加超时error,因此用户态需要注意处理方式。踩过坑
                    */
                        goto out;

                err = sock_intr_errno(timeo);
                if (signal_pending(current))
                        goto out;
        }

        /* Connection was closed by RST, timeout, ICMP error
         * or another process disconnected us.
         */
        if (sk->sk_state == TCP_CLOSE) //查看tcp连接状态。如果TCP状态没有改变说明没有收到 syn/ack报文
                goto sock_error;
        /* sk->sk_err may be not zero now, if RECVERR was ordered by user
         * and error was received after socket entered established state.
         * Hence, it is handled normally after connect() return successfully.
         */

        sock->state = SS_CONNECTED; //修改状态为连接完成
        err = 0;
out:
        return err;

sock_error:
        err = sock_error(sk) ? : -ECONNABORTED;
        sock->state = SS_UNCONNECTED;
        if (sk->sk_prot->disconnect(sk, flags))
                sock->state = SS_DISCONNECTING;
        goto out;
}

sk->sk_prot->connect 在tcp中调用的是tcp_v4_connect。 这个函数的作用,按照代码顺序分为以下几个:
1.如果用户已经设置源IP,则直接使用设置的源IP
2.如果没有设置源IP则根据目的ip和路由查找源IP
3.如果用户已经设置源port,则直接使用源Port
4.如果没有设置源PORT 根据目的IP port 和当前系统的establish tcp hash表查找sport。
5.查找目的路由
6.将TCP sock 状态设置为TCP_SYN_SENT。
7.根据已经查找到的路由及路由出口设备的能力设置一些GSO TSO标志等。sk_setup_caps。
这个函数比较简单,这里就不列出来了。需要注意的是查找可用源PORT的函数,会在查找到源port后将sock添加到establish hash表中。这个时候sock状态还没有establish。个人认为 主要是为了区分listen。listen 五元组不全, establish 是全五元组。函数是inet_hash_connect。有兴趣可以进去看看。
tcp connect 关键函数是tcp_connect 函数体如下

int tcp_connect(struct sock *sk)
{       
        struct tcp_sock *tp = tcp_sk(sk);
        struct sk_buff *buff;
        int err;

        tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL);

        if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk))
                return -EHOSTUNREACH; /* Routing failure or similar. */

        tcp_connect_init(sk);  //连接初始化,这个里面会设置一些超时时间等

        if (unlikely(tp->repair)) {
                tcp_finish_connect(sk, NULL);
                return 0;
        }

        buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
        if (unlikely(!buff))
                return -ENOBUFS;

        tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
        tcp_mstamp_refresh(tp);
        tp->retrans_stamp = tcp_time_stamp(tp);
        tcp_connect_queue_skb(sk, buff);        
        tcp_ecn_send_syn(sk, buff);
        tcp_rbtree_insert(&sk->tcp_rtx_queue, buff);

        /* Send off SYN; include data in Fast Open. */ 
        err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
              tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); //发送数据包
        if (err == -ECONNREFUSED)            
                return err;

        /* We change tp->snd_nxt after the tcp_transmit_skb() call
         * in order to make this packet get counted in tcpOutSegs.
         */
        tp->snd_nxt = tp->write_seq;
        tp->pushed_seq = tp->write_seq;
        buff = tcp_send_head(sk);
        if (unlikely(buff)) {   
                tp->snd_nxt     = TCP_SKB_CB(buff)->seq;
                tp->pushed_seq  = TCP_SKB_CB(buff)->seq;
        }
        TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);

        /* Timer for repeating the SYN until an answer. */
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                                  inet_csk(sk)->icsk_rto, TCP_RTO_MAX); //设置超时重发,使用的retrans机制。
        return 0;
}

在tcp连接中有三个定时器。比较重要
1.icsk_retransmit_timer 超时重发,handler tcp_write_timer
2.icsk_delack_timer 延迟回复ack。减少ack数量handler tcp_delack_timer
3.sk_timer keepalive_handler 这个不是tcp专有,连接保活用的。handler tcp_keepalive_timer

如果超时没有收到 服务器端返回的syn/ack 报文,当前设置为1秒,则会进入超时重发逻辑,进入tcp_write_timer_handler。 这个函数根据超时事件,进行不同的处。如果是超时重发则进入 tcp_retransmit_timer中处理,这个函数将重试发包。如果超过重试次数,则会进入 tcp_write_err函数,唤醒阻塞的实体处理,调用sk->sk_error_report (sock_def_error_report)。
tcp connect到这儿发送部分基本就完了,下节分析一下 收到syn/ack后,修改TCP状态到ESTABLISH。

分类
linux network

linux socket REUSEPORT UDP socket内核代码分析

REUSEPORT 和REUSEADDR是差不多的功能,都是一个端口可以多个socket进行监听。差别在于 REUSEPORT可以做负载,且不会引起惊群效应。
REUSEPORT设置方式和REUSEADDR设置方式一样都是通过setsockopt进行设置,最终在sk上设置sk_reuseport的值。
在绑定时,会调用一系列函数,这些函数在REUSEADDR中已经讲过。最终会调用到udp_lib_lport_inuse2函数

static int udp_lib_lport_inuse2(struct net *net, __u16 num,
                                struct udp_hslot *hslot2,
                                struct sock *sk)
{
        struct sock *sk2;
        kuid_t uid = sock_i_uid(sk);
        int res = 0;

        spin_lock(&hslot2->lock);
        udp_portaddr_for_each_entry(sk2, &hslot2->head) {
                if (net_eq(sock_net(sk2), net) &&
                    sk2 != sk &&
                    (udp_sk(sk2)->udp_port_hash == num) &&
                    (!sk2->sk_reuse || !sk->sk_reuse) &&
                    (!sk2->sk_bound_dev_if || !sk->sk_bound_dev_if ||
                     sk2->sk_bound_dev_if == sk->sk_bound_dev_if) &&
                    inet_rcv_saddr_equal(sk, sk2, true)) {          //首先检查是否有冲突,如果有冲突走if中的逻辑
                        if (sk2->sk_reuseport && sk->sk_reuseport &&
                            !rcu_access_pointer(sk->sk_reuseport_cb) &&     //判断两个socket 是否都开启了sk_reuseport功能,并且最新的socket没有初始化sk_reuseport_cb。则表示端口可用返回
                            uid_eq(uid, sock_i_uid(sk2))) {
                                res = 0;
                        } else {
                                res = 1;
                        }
                        break;
                }
        }
        spin_unlock(&hslot2->lock);
        return res;
}

返回到 udp_lib_get_port函数中。会执行以下代码

found:
        inet_sk(sk)->inet_num = snum;
        udp_sk(sk)->udp_port_hash = snum;
        udp_sk(sk)->udp_portaddr_hash ^= snum;
        if (sk_unhashed(sk)) {
                if (sk->sk_reuseport &&
                    udp_reuseport_add_sock(sk, hslot)) {            //将sk按照resuseport处理以下
                        inet_sk(sk)->inet_num = 0;
                        udp_sk(sk)->udp_port_hash = 0;
                        udp_sk(sk)->udp_portaddr_hash ^= snum;
                        goto fail_unlock;
                }

                sk_add_node_rcu(sk, &hslot->head);
                hslot->count++;
                sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);

                hslot2 = udp_hashslot2(udptable, udp_sk(sk)->udp_portaddr_hash);
                spin_lock(&hslot2->lock);
                if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport &&
                    sk->sk_family == AF_INET6)
                        hlist_add_tail_rcu(&udp_sk(sk)->udp_portaddr_node,
                                           &hslot2->head);
                else
                        hlist_add_head_rcu(&udp_sk(sk)->udp_portaddr_node,
                                           &hslot2->head);          //添加sk到hash表中
                hslot2->count++;
                spin_unlock(&hslot2->lock);
        }
        sock_set_flag(sk, SOCK_RCU_FREE);
        error = 0;

udp_reuseport_add_sock 函数是是新加函数,这个函数是REUSEPORT的关键。代码分析。

static int udp_reuseport_add_sock(struct sock *sk, struct udp_hslot *hslot)
{
        struct net *net = sock_net(sk);
        kuid_t uid = sock_i_uid(sk);
        struct sock *sk2;

        sk_for_each(sk2, &hslot->head) {
                if (net_eq(sock_net(sk2), net) &&
                    sk2 != sk &&
                    sk2->sk_family == sk->sk_family &&
                    ipv6_only_sock(sk2) == ipv6_only_sock(sk) &&
                    (udp_sk(sk2)->udp_port_hash == udp_sk(sk)->udp_port_hash) &&
                    (sk2->sk_bound_dev_if == sk->sk_bound_dev_if) &&
                    sk2->sk_reuseport && uid_eq(uid, sock_i_uid(sk2)) &&        //同一用户才能共享。
                    inet_rcv_saddr_equal(sk, sk2, false)) {
                        return reuseport_add_sock(sk, sk2); //端口已经被使用多次
                }
        }

        return reuseport_alloc(sk);         //这个端口第一次被使用
}

reuseport_alloc函数如下

int reuseport_alloc(struct sock *sk)
{
        struct sock_reuseport *reuse;

        /* bh lock used since this function call may precede hlist lock in
         * soft irq of receive path or setsockopt from process context
         */
        spin_lock_bh(&reuseport_lock);

        /* Allocation attempts can occur concurrently via the setsockopt path
         * and the bind/hash path.  Nothing to do when we lose the race.
         */
        if (rcu_dereference_protected(sk->sk_reuseport_cb,
                                      lockdep_is_held(&reuseport_lock)))
                goto out;

        reuse = __reuseport_alloc(INIT_SOCKS);
        if (!reuse) {
                spin_unlock_bh(&reuseport_lock);
                return -ENOMEM;
        }

        reuse->socks[0] = sk;
        reuse->num_socks = 1;
        rcu_assign_pointer(sk->sk_reuseport_cb, reuse);     //创建并初始化reuse结构体

out:
        spin_unlock_bh(&reuseport_lock);

        return 0;
}

第一个开启reuseport的socket 初始化流程使用的是上面的流程,后续使用reuseport_add_sock,代码如下

int reuseport_add_sock(struct sock *sk, struct sock *sk2)
{       
        struct sock_reuseport *old_reuse, *reuse;

        if (!rcu_access_pointer(sk2->sk_reuseport_cb)) {
                int err = reuseport_alloc(sk2);

                if (err)
                        return err;
        }       //如果sk2没有reuse结构体,分配一个新的

        spin_lock_bh(&reuseport_lock);
        reuse = rcu_dereference_protected(sk2->sk_reuseport_cb,
                                          lockdep_is_held(&reuseport_lock));
        old_reuse = rcu_dereference_protected(sk->sk_reuseport_cb,
                                             lockdep_is_held(&reuseport_lock));
        if (old_reuse && old_reuse->num_socks != 1) {       //如果sk已经有resue结构体,并且已经和其他sk组合,则失败。
                spin_unlock_bh(&reuseport_lock);
                return -EBUSY;
        }

        if (reuse->num_socks == reuse->max_socks) {     //如果超过最多组合数,则扩整最多组合数。
                reuse = reuseport_grow(reuse);
                if (!reuse) {
                        spin_unlock_bh(&reuseport_lock);
                        return -ENOMEM;
                }
        }

        reuse->socks[reuse->num_socks] = sk;        //将新的sk加入reuse组合里面
        /* paired with smp_rmb() in reuseport_select_sock() */
        smp_wmb();
        reuse->num_socks++;
        rcu_assign_pointer(sk->sk_reuseport_cb, reuse);

        spin_unlock_bh(&reuseport_lock);

        if (old_reuse)
                call_rcu(&old_reuse->rcu, reuseport_free_rcu);      //释放就得reuse。如果需要释放的话。
        return 0;
}

这个函数很简单,就是将所有reuseport的sk放在一个数组里面。等接收到数据包时,可以根据数组来里面的sk数量通过一定的算法来负载均衡。(没有最大限制,初始化最大存放128个,如果超过则以2的指数增长,直到不能分配更大的内存为止。)
通过目的地址和目的端口找到sk以后,如果发现sk有reuse_port标志,则会调用reuseport_select_sock,使用原地址和源端口,目的地址 目的端口做hash。选择组合里面的一个sk。代码如下

                                hash = udp_ehashfn(net, daddr, hnum,
                                                   saddr, sport);
                                result = reuseport_select_sock(sk, hash, skb,
                                                        sizeof(struct udphdr));
struct sock *reuseport_select_sock(struct sock *sk,
                                   u32 hash,
                                   struct sk_buff *skb,
                                   int hdr_len)
{
        struct sock_reuseport *reuse;
        struct bpf_prog *prog; 
        struct sock *sk2 = NULL;
        u16 socks;

        rcu_read_lock();
        reuse = rcu_dereference(sk->sk_reuseport_cb);

        /* if memory allocation failed or add call is not yet complete */
        if (!reuse)
                goto out;

        prog = rcu_dereference(reuse->prog);
        socks = READ_ONCE(reuse->num_socks);
        if (likely(socks)) {
                /* paired with smp_wmb() in reuseport_add_sock() */
                smp_rmb();

                if (prog && skb)
                        sk2 = run_bpf(reuse, socks, prog, skb, hdr_len);        //bpf简直无处不在啊,现在内核里面各处都可以执行bpf,以后可以在点位或者kprobe给 内核下各种各样的规则啊。

                /* no bpf or invalid bpf result: fall back to hash usage */
                if (!sk2)
                        sk2 = reuse->socks[reciprocal_scale(hash, socks)];      //根据hash选择合适的sk返回
        }

out:
        rcu_read_unlock();
        return sk2;
}

然后,返回之后,会唤醒选定的sk对应的进程,接受数据包处理。

reuseport优势
如果在没有reuseport之前,我们使用一个进程等待网络事件,然后通过IPC通知其他进程处理业务,这中间涉及到IPC,并且可能引起进程切换,降低性能。 又有REUSEPORT后,可以使用多个进程同时在一个端口等待,根据4元组,将数据hash到不同的进程处理,不需要IPC。可以提升性能。

分类
linux file system linux network

linux socket REUSEADDR 内核代码分析

第一次接触REUSEADDR是在一年前,那个时候写了一个网络服务程序。写了一个监控脚本监控网络服务,当服务挂了,立马拉起来。但是经常会遇到拉不起来的情况,说端口已经被占用。但是那个端口自有我自己在用。怎么会被占用呢。后来查到说进程挂了,但是内核中连接还在。所以不能立马用。加上REUSEADDR就可以解决问题。今天挖掘一下内核代码。分析一下REUSEADDR实现。
创建socket

int __sys_socket(int family, int type, int protocol)
{
        int retval;
        struct socket *sock;
        int flags;

        /* Check the SOCK_* constants for consistency.  */
        BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
        BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
        BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
        BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

        flags = type & ~SOCK_TYPE_MASK;
        if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
                return -EINVAL;
        type &= SOCK_TYPE_MASK;

        if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
                flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

        retval = sock_create(family, type, protocol, &sock);            //根据用户传入的参数选择对应的协议创建对应的socket
        if (retval < 0)
                return retval;

        return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));     //将socket和文件描述符绑定。
}

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
        return __sys_socket(family, type, protocol);
}

在linux中一切皆文件,sock_map_fd函数将sock和文件关联起来,返回给用户一个文件描述符。

static int sock_map_fd(struct socket *sock, int flags)
{
        struct file *newfile;
        int fd = get_unused_fd_flags(flags);
        if (unlikely(fd < 0)) {
                sock_release(sock); 
                return fd;
        }

        newfile = sock_alloc_file(sock, flags, NULL);           //创建文件,
        if (likely(!IS_ERR(newfile))) {
                fd_install(fd, newfile);            //将文件和文件描述符关联
                return fd;                          //返回文件描述符
        }

        put_unused_fd(fd);
        return PTR_ERR(newfile);
}

sock_alloc_file是创建sock匿名文件系统的文件。代码如下

struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
        struct qstr name = { .name = "" };
        struct path path;
        struct file *file;

        if (dname) {
                name.name = dname; 
                name.len = strlen(name.name);
        } else if (sock->sk) {
                name.name = sock->sk->sk_prot_creator->name;
                name.len = strlen(name.name);
        }       
        path.dentry = d_alloc_pseudo(sock_mnt->mnt_sb, &name);
        if (unlikely(!path.dentry)) {
                sock_release(sock);
                return ERR_PTR(-ENOMEM);
        }       
        path.mnt = mntget(sock_mnt);

        d_instantiate(path.dentry, SOCK_INODE(sock));

        file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
                  &socket_file_ops);            //创建文件,文件的读写函数是socket_file_ops指定。
        if (IS_ERR(file)) {
                /* drop dentry, keep inode for a bit */
                ihold(d_inode(path.dentry));
                path_put(&path);
                /* ... and now kill it properly */
                sock_release(sock);
                return file;
        }

        sock->file = file;
        file->f_flags = O_RDWR | (flags & O_NONBLOCK);
        file->private_data = sock;
        return file;
}       

创建问socket 后使用setsockopt设置REUSEADDR。代码如下

static int __sys_setsockopt(int fd, int level, int optname,
                            char __user *optval, int optlen)
{       
        int err, fput_needed;
        struct socket *sock;

        if (optlen < 0)
                return -EINVAL;

        sock = sockfd_lookup_light(fd, &err, &fput_needed);
        if (sock != NULL) {
                err = security_socket_setsockopt(sock, level, optname);
                if (err)
                        goto out_put;

                if (level == SOL_SOCKET)            //第二个参数时这个 调用sock_setsockopt函数
                        err =
                            sock_setsockopt(sock, level, optname, optval,
                                            optlen);            //调用该函数设置 REUSEADDR。
                else
                        err =
                            sock->ops->setsockopt(sock, level, optname, optval,
                                                  optlen);
out_put:
                fput_light(sock->file, fput_needed);
        }
        return err;
}               

SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
                char __user *, optval, int, optlen)
{               
        return __sys_setsockopt(fd, level, optname, optval, optlen);
}  

sock_setsockopt函数非常长,因为包含很多参数,所以函数很长,但是逻辑简单,关键代码如下

        case SO_REUSEADDR:
                sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE);      //如果使用REUSEADDR 则sk->sk_reuse置位
                break;

最后看bind 系统调用

int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
        struct socket *sock;    
        struct sockaddr_storage address;
        int err, fput_needed;

        sock = sockfd_lookup_light(fd, &err, &fput_needed);
        if (sock) {
                err = move_addr_to_kernel(umyaddr, addrlen, &address);
                if (err >= 0) {
                        err = security_socket_bind(sock,
                                                   (struct sockaddr *)&address,
                                                   addrlen);
                        if (!err)
                                err = sock->ops->bind(sock,
                                                      (struct sockaddr *)
                                                      &address, addrlen);       //最后调用4层协议的bind函数,接下来以udp为例分析
                }
                fput_light(sock->file, fput_needed);
        }
        return err;
}

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
        return __sys_bind(fd, umyaddr, addrlen);
}

以 IPV4 udp 为例,可以得知 sock->ops->bind最终调用的函数是inet_bind函数,函数在net/ipv4/af_inet.c文件中。inet_bind 经过一些安全检查调用 __inet_bind函数, __inet_bind函数中会调用协议对应的get_port函数,判断绑定的端口是否能够被使用。

        if (snum || !(inet->bind_address_no_port ||
                      force_bind_address_no_port)) {
                if (sk->sk_prot->get_port(sk, snum)) {          //如果返回值非零,表示地址已经被使用
                        inet->inet_saddr = inet->inet_rcv_saddr = 0;
                        err = -EADDRINUSE;
                        goto out_release_sock;
                }
                err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);
                if (err) {
                        inet->inet_saddr = inet->inet_rcv_saddr = 0;
                        goto out_release_sock;
                }
        }

以ipv4 udp为例子,get_port调用的是udp_v4_get_port 文件位置是net/ipv4/udp.c
udp_v4_get_port函数最终调用udp_lib_get_port函数判断地址是否被使用。udp_lib_get_port调用udp_lib_lport_inuse2判断地址是否被使用。
udp_lib_lport_inuse2函数

static int udp_lib_lport_inuse2(struct net *net, __u16 num, 
                                struct udp_hslot *hslot2,
                                struct sock *sk) 
{
        struct sock *sk2;
        kuid_t uid = sock_i_uid(sk);
        int res = 0; 

        spin_lock(&hslot2->lock);
        udp_portaddr_for_each_entry(sk2, &hslot2->head) {
                if (net_eq(sock_net(sk2), net) &&
                    sk2 != sk &&
                    (udp_sk(sk2)->udp_port_hash == num) &&
                    (!sk2->sk_reuse || !sk->sk_reuse) &&                //如果都设置了reuse,则跳过sk2 继续查找下一个。
                    (!sk2->sk_bound_dev_if || !sk->sk_bound_dev_if ||
                     sk2->sk_bound_dev_if == sk->sk_bound_dev_if) &&
                    inet_rcv_saddr_equal(sk, sk2, true)) {
                        if (sk2->sk_reuseport && sk->sk_reuseport &&
                            !rcu_access_pointer(sk->sk_reuseport_cb) &&
                            uid_eq(uid, sock_i_uid(sk2))) {
                                res = 0; 
                        } else {
                                res = 1; 
                        }    
                        break;
                }    
        }    
        spin_unlock(&hslot2->lock);
        return res; 
}

由上面的代码可以看出,在udp模式下,允许两个socket共享一个地址。而不报错。

进一步分析代码,我们可以发现,只是使用REUSEADDR,虽然在同一个地址上创建多个socket,但是只有最后一个socket是收包的,其他socket不收包,做不到多个socket 负载均衡的功能,要想使用负载均衡的功能需要使用REUSEPORT参数。在上面代码里面已经隐隐约约能看到REUSEPORT参数的代码。我们下街分析。