美文网首页
bind 如何绑定端口

bind 如何绑定端口

作者: 董泽润 | 来源:发表于2018-08-22 12:14 被阅读253次

    当套接字创建后,服务端需要调用函数 bind 来绑定监听端口,看似一句话很简单,那他是如何实现的呢?


    bind函数使用

    先看一下函数声明

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    

    第一个整形参数是己经创建的 socket 文件描述符,第二个是传入的地址结构体。由于要兼容老系统,函数声明类型是 struct sockaddr *, 但是我们一般使用 sockaddr_in, 后者更易于使用。

    struct sockaddr {
        sa_family_t sa_family;  /* address family, AF_xxx   */
        char        sa_data[14];    /* 14 bytes of protocol address */
    };
    
    struct sockaddr_in {
      __kernel_sa_family_t  sin_family; /* Address family       */
      __be16        sin_port;   /* Port number          */
      struct in_addr    sin_addr;   /* Internet address     */
    
      /* Pad to size of `struct sockaddr'. */
      unsigned char     __pad[__SOCK_SIZE__ - sizeof(short int) -
                sizeof(unsigned short int) - sizeof(struct in_addr)];
    };
    

    可以看到,实际上两个结构体大小都是 16 字节,不足的部份使用 pad 填充。sockaddr_in 有成员 sin_port, sin_addr 使用更方便。

    绑定流程

    查看系统调用,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);
            }
            fput_light(sock->file, fput_needed);
        }
        return err;
    }
    

    1.由于传入的是 fd, 所以要调用 sockfd_lookup_light 查找到 socket 结构,如果失败返回。这块代码很简单,从当前进程 PCB 打开文件表里找到对应 fdfile. 在创建 socket 时,sock_map_fdfilesocket 关联,所以是一一对应关系
    2.内核拷贝地址结构,move_addr_to_kernel, 从用户空间将 sockaddr 拷贝到内核空间,涉及到地址空间有效性检查。
    3.调用 socket 指定协义的 sock->ops->bind 回调函数,完成绑定。在创建套接字时得知,sock->ops 被赋值为接口 inet_stream_ops, 而调用 bind 最终会调用 inet_bind

    inet_bind 实现

    查看 af_inet.c 文件,inet_bind 函数除了 raw 类型的套接字都会调用 __inet_bind. 分段来看实现

    int __inet_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len,
            bool force_bind_address_no_port, bool with_lock)
    {
        struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
        struct inet_sock *inet = inet_sk(sk);
        struct net *net = sock_net(sk);
        unsigned short snum;
        int chk_addr_ret;
        u32 tb_id = RT_TABLE_LOCAL;
        int err;
    
        if (addr->sin_family != AF_INET) {
            /* Compatibility games : accept AF_UNSPEC (mapped to AF_INET)
             * only if s_addr is INADDR_ANY.
             */
            err = -EAFNOSUPPORT;
            if (addr->sin_family != AF_UNSPEC ||
                addr->sin_addr.s_addr != htonl(INADDR_ANY))
                goto out;
        }
    
        tb_id = l3mdev_fib_table_by_index(net, sk->sk_bound_dev_if) ? : tb_id;
        chk_addr_ret = inet_addr_type_table(net, addr->sin_addr.s_addr, tb_id);
    

    两个函数 l3mdev_fib_table_by_index, inet_addr_type_table 涉及路由,功能就是根据传入的地址 s_addr 来判断是多播,广播,还是单播。比如地址是 0.0.0.0 就是广播,192.168.1.34 就是单播,其它广播,多播地址类型由以上两个函数确定。只有 udp 使用,忽略细节。

        /* Not specified by any standard per-se, however it breaks too
         * many applications when removed.  It is unfortunate since
         * allowing applications to make a non-local bind solves
         * several problems with systems using dynamic addressing.
         * (ie. your servers still start up even if your ISDN link
         *  is temporarily down)
         */
        err = -EADDRNOTAVAIL;
        if (!net->ipv4.sysctl_ip_nonlocal_bind &&
            !(inet->freebind || inet->transparent) &&
            addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
            chk_addr_ret != RTN_LOCAL &&
            chk_addr_ret != RTN_MULTICAST &&
            chk_addr_ret != RTN_BROADCAST)
            goto out;
    
        snum = ntohs(addr->sin_port);
        err = -EACCES;
        if (snum && snum < inet_prot_sock(net) &&
            !ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
            goto out;
    

    ntohs 将网络大端端口转换成主机测的端口,inet_prot_sock 用来判断申请的端口是否小于在 1024,由于这些都是有限制的, 普通用户无法使用,ns_capable 没有权限就会退出。

        /*      We keep a pair of addresses. rcv_saddr is the one
         *      used by hash lookups, and saddr is used for transmit.
         *
         *      In the BSD API these are the same except where it
         *      would be illegal to use them (multicast/broadcast) in
         *      which case the sending device address is used.
         */
        if (with_lock)
            lock_sock(sk);
    
        /* Check these errors (active socket, double bind). */
        err = -EINVAL;
        if (sk->sk_state != TCP_CLOSE || inet->inet_num)
            goto out_release_sock;
    

    查看套接字当前的状态,不处于关闭状态,或是己经绑定端口了,肯定是不可以再绑定。

        inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
        if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
            inet->inet_saddr = 0;  /* Use device */
    

    对于多播,广播地址,源地址肯定是不能确认的,需要有对端连接,跟据路由来智能识别。所以 inet_saddr 置 0

        /* Make sure we are allowed to bind here. */
        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;
            }
        }
    

    sk->sk_prot->get_port 核心的设置端口函数,在创建 socket 时可知,sk_prot 指向 tcp_prot, 所以最终 get_port 调用 tcp_prot.inet_csk_get_port,这是和协义相关的最核心代码。

    
        if (inet->inet_rcv_saddr)
            sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
        if (snum)
            sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
        inet->inet_sport = htons(inet->inet_num);
        inet->inet_daddr = 0;
        inet->inet_dport = 0;
        sk_dst_reset(sk);
        err = 0;
    out_release_sock:
        if (with_lock)
            release_sock(sk);
    out:
        return err;
    }
    

    如果 inet_rcv_saddr 不为 0 ,那么设置锁。如果 snum 不为 0,设置锁。并将目地地址,端口置 0. 另外 sk_dst_reset 涉及路由,暂时也忽略。

    inet_csk_get_port 实现

    分析代码前先想个问题,bind 端口时有哪些注意点呢?换句话说,如果是我们自己实现,会怎么写呢。

    1. 如果 bind 时不指定端口,那系统会怎么挑选端口?
    2. 如果指定的端口被占用了,系统会不会强制使用?
    3. 内核如何保存所有 socket 连接?怎样做到高效的冲突检测
    4. SO_REUSEPORT 是如何工作的呢?

    代码分段阅读

    /* Obtain a reference to a local port for the given sock,
     * if snum is zero it means select any available local port.
     * We try to allocate an odd port (and leave even ports for connect())
     */
    int inet_csk_get_port(struct sock *sk, unsigned short snum)
    {
        bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;
        struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
        int ret = 1, port = snum;
        struct inet_bind_hashbucket *head;
        struct net *net = sock_net(sk);
        struct inet_bind_bucket *tb = NULL;
        kuid_t uid = sock_i_uid(sk);
    

    这里重点是 h.hashinfo,他的类型是 inet_hashinfo, 通过 sk->sk_prot 得知,hashinfo 是一个全局单例,有两个核心成员 inet_ehash_bucketinet_bind_hashbucket 分别保存己建立连接的 socket 集合,和己 bind 的集合。类似哈希桶的实现,根据端口来找到桶,然后桶里的数据用链表来维护。

        if (!port) {
            head = inet_csk_find_open_port(sk, &tb, &port);
            if (!head)
                return ret;
            if (!tb)
                goto tb_not_found;
            goto success;
        }
    

    如果没有指定端口,那么系统会根据 inet_csk_find_open_port 来分配,这个一会回过头再看实现。

        head = &hinfo->bhash[inet_bhashfn(net, port,
                          hinfo->bhash_size)];
        spin_lock_bh(&head->lock);
        inet_bind_bucket_for_each(tb, &head->chain)
            if (net_eq(ib_net(tb), net) && tb->port == port)
                goto tb_found;
    tb_not_found:
        tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
                         net, head, port);
        if (!tb)
            goto fail_unlock;
    

    函数 inet_bhashfn 是哈希算法,主要是看端口。得到对应桶,一个链表的 header, 如果桶不为空,说明很可能端口己经被占用。如果桶为空,那一定没有被占用,调用 inet_bind_bucket_create 初始化桶。

    inet_bind_bucket_for_each 宏用来遍历链表,如果网络设备相同,并且端口相等,那说明找到了,但是能否使用,还不一定。

    tb_found:
        if (!hlist_empty(&tb->owners)) {
            if (sk->sk_reuse == SK_FORCE_REUSE)
                goto success;
    
            if ((tb->fastreuse > 0 && reuse) ||
                sk_reuseport_match(tb, sk))
                goto success;
            if (inet_csk_bind_conflict(sk, tb, true, true))
                goto fail_unlock;
        }
    

    tb 的类型是 inet_bind_bucket, 看下他的定义得知,tb->owners 也是个链表,因为端口可以被多个 socket 同时绑定。如果使用者链表不为空,那么就是检查当前 sk->sk_reuse 是否强制,sk_reuseport_match 检查是否可重用,inet_csk_bind_conflict 检查是否冲突。

    sk_reuseport_match
    static inline int sk_reuseport_match(struct inet_bind_bucket *tb,
                         struct sock *sk)
    {
        kuid_t uid = sock_i_uid(sk);
    
        if (tb->fastreuseport <= 0)
            return 0;
        if (!sk->sk_reuseport)
            return 0;
        if (rcu_access_pointer(sk->sk_reuseport_cb))
            return 0;
        if (!uid_eq(tb->fastuid, uid))
            return 0;
        /* We only need to check the rcv_saddr if this tb was once marked
         * without fastreuseport and then was reset, as we can only know that
         * the fast_*rcv_saddr doesn't have any conflicts with the socks on the
         * owners list.
         */
        if (tb->fastreuseport == FASTREUSEPORT_ANY)
            return 1;
    #if IS_ENABLED(CONFIG_IPV6)
        if (tb->fast_sk_family == AF_INET6)
            return ipv6_rcv_saddr_equal(&tb->fast_v6_rcv_saddr,
                            inet6_rcv_saddr(sk),
                            tb->fast_rcv_saddr,
                            sk->sk_rcv_saddr,
                            tb->fast_ipv6_only,
                            ipv6_only_sock(sk), true);
    #endif
        return ipv4_rcv_saddr_equal(tb->fast_rcv_saddr, sk->sk_rcv_saddr,
                        ipv6_only_sock(sk), true);
    }
    
    1. 如果 tb->fastreuseport 或是 sk-> sk_reuseport 没有打开,那么肯定不可以重用
    2. 检查 uid, 必须是同一个用户下的才可以
    3. 如果当前 tb->fastreuseportFASTREUSEPORT_ANY 那么无条件匹配
    4. 调用 ipv4_rcv_saddr_equal 判断是否匹配,如果最后一个参数是 true, 打开了泛匹配,那么 0.0.0.0可以匹配任意 ip 地址
    inet_csk_bind_conflict
    static int inet_csk_bind_conflict(const struct sock *sk,
                      const struct inet_bind_bucket *tb,
                      bool relax, bool reuseport_ok)
    {
        struct sock *sk2;
        bool reuse = sk->sk_reuse;
        bool reuseport = !!sk->sk_reuseport && reuseport_ok;
        kuid_t uid = sock_i_uid((struct sock *)sk);
        /*
         * Unlike other sk lookup places we do not check
         * for sk_net here, since _all_ the socks listed
         * in tb->owners list belong to the same net - the
         * one this bucket belongs to.
         */
        sk_for_each_bound(sk2, &tb->owners) {
            if (sk != 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) &&
                    (!reuseport || !sk2->sk_reuseport ||
                     rcu_access_pointer(sk->sk_reuseport_cb) ||
                     (sk2->sk_state != TCP_TIME_WAIT &&
                     !uid_eq(uid, sock_i_uid(sk2))))) {
                    if (inet_rcv_saddr_equal(sk, sk2, true))
                        break;
                }
                if (!relax && reuse && sk2->sk_reuse &&
                    sk2->sk_state != TCP_LISTEN) {
                    if (inet_rcv_saddr_equal(sk, sk2, true))
                        break;
                }
            }
        }
        return sk2 != NULL;
    }
    

    这个函数用来检查是否冲突,刚才说了 tb->owners 是一个链表,保存的是共用这个 inet_bind_bucketsocket 集合,也就是说如果想重用这个端口,还要遍历。

    1. sk != sk2 如果是自己肯定跳过
    2. 任一一个没有 bindtodeivce ,或是 bound 了同一个需要继续检查,如果分别 bound 不同设备,那肯定不冲突
    3. tb 连接处于 TCP_LISTEN 状态,并且设置了 sk_reuseport_cb 不可以重用
    4. tb 连接如果不处于 TCP_TIME_WAIT状态,并且不是同一个用户,不可重用

    回头看 inet_csk_get_port 主流程

    success:
        if (hlist_empty(&tb->owners)) {
            tb->fastreuse = reuse;
            if (sk->sk_reuseport) {
                tb->fastreuseport = FASTREUSEPORT_ANY;
                tb->fastuid = uid;
                tb->fast_rcv_saddr = sk->sk_rcv_saddr;
                tb->fast_ipv6_only = ipv6_only_sock(sk);
                tb->fast_sk_family = sk->sk_family;
    #if IS_ENABLED(CONFIG_IPV6)
                tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
    #endif
            } else {
                tb->fastreuseport = 0;
            }
    

    此时如果 tb->owners 为空,说明是第一次分配,被始化 tb 即可

        } else {
            if (!reuse)
                tb->fastreuse = 0;
            if (sk->sk_reuseport) {
                /* We didn't match or we don't have fastreuseport set on
                 * the tb, but we have sk_reuseport set on this socket
                 * and we know that there are no bind conflicts with
                 * this socket in this tb, so reset our tb's reuseport
                 * settings so that any subsequent sockets that match
                 * our current socket will be put on the fast path.
                 *
                 * If we reset we need to set FASTREUSEPORT_STRICT so we
                 * do extra checking for all subsequent sk_reuseport
                 * socks.
                 */
                if (!sk_reuseport_match(tb, sk)) {
                    tb->fastreuseport = FASTREUSEPORT_STRICT;
                    tb->fastuid = uid;
                    tb->fast_rcv_saddr = sk->sk_rcv_saddr;
                    tb->fast_ipv6_only = ipv6_only_sock(sk);
                    tb->fast_sk_family = sk->sk_family;
    #if IS_ENABLED(CONFIG_IPV6)
                    tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
    #endif
                }
            } else {
                tb->fastreuseport = 0;
            }
        }
        if (!inet_csk(sk)->icsk_bind_hash)
            inet_bind_hash(sk, tb, port);
        WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
        ret = 0;
    
    fail_unlock:
        spin_unlock_bh(&head->lock);
        return ret;
    }
    EXPORT_SYMBOL_GPL(inet_csk_get_port);
    

    最后最重要的是调用 inet_bind_hashsocket, tb, port 关联。看下函数代码

    void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb,
                const unsigned short snum)
    {
        inet_sk(sk)->inet_num = snum;
        sk_add_bind_node(sk, &tb->owners);
        inet_csk(sk)->icsk_bind_hash = tb;
    }
    

    snum 即端口赋值给 inet_num, 在 tb 的拥有者链表里把自己注册进去,将 tb 赋值给自己的成员 icsk_bind_hash, 说白了就是互相引用,你中有我,我中有你,方便访问。

    inet_csk_find_open_port 自动分配端口

    前面挖了一个坑,如果没有指定端口,系统会自动分配。我们都知道端口分配是有范围的,由参数 net.ipv4.ip_local_port_range 控制,我的测试机是从 2048 到 65000. 这里不贴源码了,bind 优先从高半部分端口开始分配,connect 系统调用会产生新的连接,优先从低半部分分配。其它的 reuse 冲突检测一个也不能少。

    小结

    没想到一个 bind 这么复杂,后面还有 listen, connect, write 等等。另外还是有疑问,如果设置了 SO_REUSEPORT, 并且多进程监听同一个端口,流量如何分配呢?以后再看吧。

    相关文章

      网友评论

          本文标题:bind 如何绑定端口

          本文链接:https://www.haomeiwen.com/subject/udsjiftx.html