Linux网络协议栈 -- socket bind 地址绑定

时间:2021-01-07 11:04:34

1、bind()

当创建了一个 Socket 套接字后,对于服务器来说,接下来的工作,就是调用 bind(2)为服务器指明本地址、协议端口号,常常可以看到这样的代码: 

strut sockaddr_in sin;  
sin.sin_family = AF_INET; 
sin.sin_addr.s_addr = xxx; 
sin.sin_port = xxx; 
bind(sock, (struct sockaddr *)&sin, sizeof(sin)); 
 
从这个系统调用中,可以知道当进行 SYS_BIND  操作的时候,: 
1、对于 AF_INET 协议簇来讲,其地址格式是 strut sockaddr_in,而对于 socket 来讲,strut sockaddr 结构表示的地址格式实现了更高层次 的抽像,因为每种协议长簇的地址不一定是相同的,所以,系统调用的第三个参数得指明该协议簇的地址格式的长度。 
 2、进行 bind(2)系统调用时,除了地址长度外,还得向内核提供:sock 描述符、协议簇名称、本地地址、端口这些参数; 
 


2、sys_bind()

操作 SYS_BIND  是由 sys_bind()实现的: 
 
asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen) 

        struct socket *sock; 
        char address[MAX_SOCK_ADDR]; 
        int err; 
 
        if((sock = sockfd_lookup(fd,&err))!=NULL) 
        { 
                if((err=move_addr_to_kernel(umyaddr,addrlen,address))>=0) { 
                        err = security_socket_bind(sock, (struct sockaddr *)address, addrlen); 
                        if (err) { 
                                sockfd_put(sock); 
                                return err; 
                        } 
                        err = sock->ops->bind(sock, (struct sockaddr *)address, addrlen); 
                } 
                sockfd_put(sock); 
        }                         
        return err; 

 
在 socket 的创建中,已经反复分析了 socket 与文件系统的关系,现在已知 socket的描述符号,要找出与之相关的 socket 结构,应该是件容易的事情: 
 
struct socket *sockfd_lookup(int fd, int *err) 

        struct file *file; 
        struct inode *inode; 
        struct socket *sock; 
         if (!(file = fget(fd))) 
        { 
                *err = -EBADF; 
                return NULL; 
        } 
 
        inode = file->f_dentry->d_inode; 
        if (!S_ISSOCK(inode->i_mode)) { 
                *err = -ENOTSOCK; 
                fput(file); 
                return NULL; 
        } 
 
        sock = SOCKET_I(inode); 
        if (sock->file != file) { 
                printk(KERN_ERR "socki_lookup: socket file changed!\n"); 
                sock->file = file; 
        } 
        return sock; 

 
fget 从当前进程的 files 指针中,根据 sock 对应的描述符号,找到已打开的文件 file,再根据文件的目录项中的 inode,利用inode 与 sock 被封装在同一个结构中的事实,调用宏 SOCKET_I找到待查的 sock 结构。最后做一个小小的判断,因为正常情况下,sock 的 file 指针是  回指与之相关的 file。  
 
接下来的工作是把用户态的地址拷贝至内核中来: 
int move_addr_to_kernel(void __user *uaddr, int ulen, void *kaddr) 

        if(ulen<0||ulen>MAX_SOCK_ADDR) 
                return -EINVAL; 
        if(ulen==0) 
                return 0; 
        if(copy_from_user(kaddr,uaddr,ulen)) 
                return -EFAULT; 
        return 0; 

 
bind(2)第三个参数必须存在的原因之一,copy_from_user 必须知道拷贝的字节长度。 
 
因为 sock 的 ops 函数指针集,在创建之初,就指向了对应的协议类型,例如如果类型是SOCK_STREAM,那么它就指向 inetsw_array[0].ops。也就是 inet_stream_ops: 
struct proto_ops inet_stream_ops = { 
        .family =        PF_INET, 
        ……         .bind =                inet_bind, 
        …… 
}; 
 
sys_bind()在做完了一个通用的 socket bind 应该做的事情,包括查找对应 sock 结构,拷贝地址。就

调用对应协议族的对应协议类型的 bind 函数,也就是 inet_bind。 


 
3、inet_bind 
说 bind()的最重要的作用,就是为套接字绑定地址和端口,那么要分析 inet_bind()之前,要搞清楚的一件事情就是,这个绑定,是绑定到哪儿?或者说,是绑定到内核的哪个数据结构的哪个成员变量上面?? 
有三个地方是可以考虑的:socket 结构,包括 sock 和 sk,inet结构,以及 protoname_sock 结构。绑定在 socket 结构上是可行  的,这样可以实现最高层面上的抽像,但是因为每一类协议簇 socket 的地址及端口表现形式差异很大,这样就得引入专门的转换处理功能。绑定在 protoname_sock 也是可行的,但是却是最笨拙的,因为例如 tcp 和 udp,它们的地址及端口表现形式是一样的,这样就浪费了空间,加大了代码  处理量。所以,inet 做为一个协议类型的抽像,是最理想的地方了,再来回顾一下它的定义: 
 
struct inet_sock { 
        …… 
        /* Socket demultiplex comparisons on incoming packets. */ 
        __u32                        daddr;                /* Foreign IPv4 addr */ 
        __u32                        rcv_saddr;        /* Bound local IPv4 addr */ 
        __u16                         dport;                /* Destination port */ 
        __u16                        num;                /* Local port */ 
        __u32                        saddr;                /* Sending source */ 
        …… 
};
 
去掉了其它成员,保留了与地址及端口相关的成员变量,从注释中,可以清楚地了解它们的作用。所以,我们说的 bind(2)之绑定,主要就是对这几个成员变量赋值的过程了。 
 
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len) 

        /*  获取地址参数 */ 
        struct sockaddr_in *addr = (struct sockaddr_in *)uaddr; 
        /*  获取 sock 对应的 sk */ 
        struct sock *sk = sock->sk; 
        /*  获取 sk 对应的inet */ 
        struct inet_sock *inet = inet_sk(sk); 
        /*  这个临时变量用来保存用户态传递下来的端口参数 */ 
        unsigned short snum; 
        int chk_addr_ret; 
        int err;  
        /*  如果协议簇对应的协议自身还有 bind 函数,调用之,例如 SOCK_RAW 就还有一个raw_bind */ 
        if (sk->sk_prot->bind) { 
                err = sk->sk_prot->bind(sk, uaddr, addr_len); 
                goto out; 
        } 
        /*  校验地址长度 */ 
        err = -EINVAL; 
        if (addr_len < sizeof(struct sockaddr_in)) 
                goto out; 
 
        /*  判断地址类型:广播?多播?单播? */ 
        chk_addr_ret = inet_addr_type(addr->sin_addr.s_addr); 
         
        /* ipv4 有一个 ip_nonlocal_bind标志,表示是否绑定非本地址 IP地址,可以通过  
         * cat /proc/sys/net/ipv4/ip_nonlocal_bind 查看到。它用来解决某些服务绑定 
         *  动态 IP地址的情况。作者在注释中已有详细说明: 
         * 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) 
         *  这里判断,用来确认如果没有开启“绑定非本地址 IP”,地址值及类型是正确的 
         */ 
        err = -EADDRNOTAVAIL; 
        if (!sysctl_ip_nonlocal_bind && 
            !inet->freebind && 
            addr->sin_addr.s_addr != 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; 
        /*  校验当前进程有没有使用低于 1024 端口的能力 */ 
        if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE)) 
                goto out; 
 
        /*      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. 
         */ 
        lock_sock(sk); 
 
        /*  检查 socket 是否已经被绑定过了:用了两个检查项,一个是 sk 状态,另一个是是否已经绑定过端口了 当然地址本来就可以为 0,所以,不能做为检查项 */ 
        err = -EINVAL; 
        if (sk->sk_state != TCP_CLOSE || inet->num) 
                goto out_release_sock; 
         
        /*  绑定 inet 的接收地址(地址服务绑定地址)和来源地址为用户态指定地址 */ 
        inet->rcv_saddr = inet->saddr = addr->sin_addr.s_addr; 
        /*  若地址类型为广播或多播,则将地址置 0,表示直接使用网络设备 */ 
        if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST) 
                inet->saddr = 0;  /* Use device */ 
 
        /*  
         *  调用协议的 get_port 函数,确认是否可绑定端口,若可以,则绑定在 inet->num 之上,注意,这里虽然没有 
         *  把inet 传过去,但是第一个参数 sk,它本身和 inet是可以互相转化的 
         */ 
        if (sk->sk_prot->get_port(sk, snum)) { 
                inet->saddr = inet->rcv_saddr = 0; 
                err = -EADDRINUSE; 
                goto out_release_sock; 
        } 
         
        /*  如果端口和地址可以绑定,置标志位 */ 
        if (inet->rcv_saddr) 
                sk->sk_userlocks |= SOCK_BINDADDR_LOCK; 
        if (snum) 
                sk->sk_userlocks |= SOCK_BINDPORT_LOCK; 
        /* inet的 sport(来源端口)成员也置为绑定端口 */ 
        inet->sport = htons(inet->num); 
        inet->daddr = 0; 
        inet->dport = 0; 
        sk_dst_reset(sk); 
        err = 0; 
out_release_sock: 
        release_sock(sk); out: 
        return err; 
}
上述分析中,忽略的第一个细节是 capable()函数调用,它是 Linux  安全模块(LSM)的一部份,简单地讲其用来对权限做出检查, 检查是否有权对指定的资源进行操作。这里它的参数是 CAP_NET_BIND_SERVICE,表示的含义是:  
/* Allows binding to TCP/UDP sockets below 1024 */ 
/* Allows binding to ATM VCIs below 32 */ 
 
#define CAP_NET_BIND_SERVICE 10
 
另一个就是协议的端口绑定,调用了协议的 get_port 函数,如果是 SOCK_STREAM的 TCP  协议,那么它就是 tcp_v4_get_port()函数。 
 
4、协议端口的绑定
 
要分配这个函数,还是得先绕一些基本的东东。这里涉及到内核中提供 hash 链表的操作的 API。可以参考其它相关资料。 
http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html
这里讲了链表的实现,顺道提了一个 hash 链表,觉得写得还不错,收藏一下。 
 
对于 TCP已注册的端口,是采用一个 hash 表来维护的。hash 桶用 struct tcp_bind_hashbucket 结构来表示: 
struct tcp_bind_hashbucket { 
        spinlock_t                lock; 
        struct hlist_head        chain; 
};
 
hash 表中的每一个 hash节点,用 struct tcp_bind_hashbucket 结构来表示: 
struct tcp_bind_bucket { 
        unsigned short                port;                        /*  节点中绑定的端口 */ 
        signed short                fastreuse; 
        struct hlist_node        node; 
        struct hlist_head        owners; 
}; 
 
tcp_hashinfo 的 hash 表信息,都集中封装在结构 tcp_hashinfo 当中,而维护已注册端口,只是它其中一部份: 
 
extern struct tcp_hashinfo { 
        …… 
 
        /* Ok, let's try this, I give up, we do need a local binding 
         * TCP hash as well as the others for fast bind/connect.          */ 
        struct tcp_bind_hashbucket *__tcp_bhash; 
 
        int __tcp_bhash_size; 
        …… 
} tcp_hashinfo; 
 
#define tcp_bhash        (tcp_hashinfo.__tcp_bhash) 
#define tcp_bhash_size        (tcp_hashinfo.__tcp_bhash_size)
 
其使用的 hash 函数是 tcp_bhashfn: 
/* These are AF independent. */ 
static __inline__ int tcp_bhashfn(__u16 lport) 

        return (lport & (tcp_bhash_size - 1)); 
}
 
这样,如果要取得某个端口对应的 hash 链的首部hash 桶节点的话,可以使用: 
struct tcp_bind_hashbucket *head; 
head = &tcp_bhash[tcp_bhashfn(snum)];
 
如果要新绑定一个端口,就是先创建一个 struct tcp_bind_hashbucket 结构的 hash 节点,然后把它插入到对应的 hash 链中去: 


struct tcp_bind_bucket *tb; 

tb = tcp_bucket_create(head, snum); 
struct tcp_bind_bucket *tcp_bucket_create(struct tcp_bind_hashbucket *head, 
                                          unsigned short snum) 

        struct tcp_bind_bucket *tb = kmem_cache_alloc(tcp_bucket_cachep, 
                                                      SLAB_ATOMIC); 
        if (tb) { 
                tb->port = snum; 
                tb->fastreuse = 0; 
                INIT_HLIST_HEAD(&tb->owners); 
                hlist_add_head(&tb->node, &head->chain); 
        } 
        return tb; 
}
 

另外,sk 中,还级护了一个类似的 hash 链表,同时需要调用 tcp_bind_hash()函数把 hash 节点插入进去: 

struct sock { 

        struct sock_common        __sk_common; 
#define sk_bind_node                __sk_common.skc_bind_node 
        …… 
}
 
/* @skc_bind_node: bind hash linkage for various protocol lookup tables */ 
struct sock_common { 
        struct hlist_node        skc_bind_node; 
        …… 
}
         
if (!tcp_sk(sk)->bind_hash) 
        tcp_bind_hash(sk, tb, snum); 
         
void tcp_bind_hash(struct sock *sk, struct tcp_bind_bucket *tb, 
                   unsigned short snum) 

        inet_sk(sk)->num = snum; 
        sk_add_bind_node(sk, &tb->owners); 
        tcp_sk(sk)->bind_hash = tb; 
}
 
这里,就顺道绑定了 inet 的 num成员变量,并置协议的 bind_hash 指针为当前分配的 hash 节点。而sk_add_bind_node 函数, 就是一个插入 hash 表节点的过程: 
 
static __inline__ void sk_add_bind_node(struct sock *sk, 
                                        struct hlist_head *list) 

        hlist_add_head(&sk->sk_bind_node, list); 
}
 
如果要遍历 hash 表的话,例如在插入之前,先判断端口是否已经在 hash表当中了。就可以调用: 
 
#define tb_for_each(tb, node, head) hlist_for_each_entry(tb, node, head, node) 
 
struct tcp_bind_hashbucket *head; 
struct tcp_bind_bucket *tb; 
 
head = &tcp_bhash[tcp_bhashfn(snum)]; 
spin_lock(&head->lock); 
tb_for_each(tb, node, &head->chain) 

        if (tb->port == snum)                 found,do_something;

             

有了这些基础知识,再来看 tcp_v4_get_port()的实现,就要容易得多了: 
 
static int tcp_v4_get_port(struct sock *sk, unsigned short snum) 

        struct tcp_bind_hashbucket *head; 
        struct hlist_node *node; 
        struct tcp_bind_bucket *tb; 
        int ret; 
 
        local_bh_disable(); 
         
        /*  如果端口值为 0,意味着让系统从本地可用端口用选择一个,并置 snum为分配的值 */ 
        if (!snum) { 
                int low = sysctl_local_port_range[0]; 
                int high = sysctl_local_port_range[1]; 
                int remaining = (high - low) + 1; 
                int rover; 
 
                spin_lock(&tcp_portalloc_lock); 
                if (tcp_port_rover < low) 
                        rover = low; 
                else 
                        rover = tcp_port_rover; 
                do { 
                        rover++; 
                        if (rover > high) 
                                rover = low; 
                         head = &tcp_bhash[tcp_bhashfn(rover)]; 
                        spin_lock(&head->lock); 
                        tb_for_each(tb, node, &head->chain) 
                                if (tb->port == rover) 
                                        goto next; 
                        break; 
                next: 
                        spin_unlock(&head->lock); 
                } while (--remaining > 0); 
                tcp_port_rover = rover; 
                spin_unlock(&tcp_portalloc_lock); 
 
                /* Exhausted local port range during search? */ 
                ret = 1; 
                if (remaining <= 0) 
                        goto fail;  
                /* OK, here is the one we will use.  HEAD is 
                 * non-NULL and we hold it's mutex. 
                 */ 
                snum = rover; 
        } else { 
                /*  否则,就在 hash 表中,查找端口是否已经存在 */ 
                head = &tcp_bhash[tcp_bhashfn(snum)]; 
                spin_lock(&head->lock); 
                tb_for_each(tb, node, &head->chain) 
                        if (tb->port == snum) 
                                goto tb_found; 
        } 
        tb = NULL; 
        goto tb_not_found; 
tb_found: 
        /*  稍后有对应的代码:第一次分配 tb 后,会调用 tcp_bind_hash加入至相应的 sk,这里先做一个判断,来确定这一步工作是否进行过*/ 
        if (!hlist_empty(&tb->owners)) { 
/* socket的 SO_REUSEADDR  选项,用来确定是否允许本地地址重用,例如同时启动多个服务器、多个套接字绑定至同一端口等等, sk_reuse 成员对应其值,因为如果一个绑定的 hash节点已经存在,而且不允许重用的话,那么则表示因冲突导致出错,调用 tcp_bind_conflict 来处理之 */ 
                if (sk->sk_reuse > 1) 
                        goto success; 
                if (tb->fastreuse > 0 && 
                    sk->sk_reuse && sk->sk_state != TCP_LISTEN) { 
                        goto success; 
                } else { 
                        ret = 1; 
                        if (tcp_bind_conflict(sk, tb)) 
                                goto fail_unlock; 
                } 
        } 
tb_not_found: 
        /*  如果不存在,则分配 hash节点,绑定端口 */ 
        ret = 1; 
        if (!tb && (tb = tcp_bucket_create(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; 
        } else if (tb->fastreuse &&                    (!sk->sk_reuse || sk->sk_state == TCP_LISTEN)) 
                tb->fastreuse = 0; 
success: 
        if (!tcp_sk(sk)->bind_hash) 
                tcp_bind_hash(sk, tb, snum); 
        BUG_TRAP(tcp_sk(sk)->bind_hash == tb); 
        ret = 0; 
 
fail_unlock: 
        spin_unlock(&head->lock); 
fail: 
        local_bh_enable(); 
        return ret; 
}
 
到这里,可以为这部份下一个小结了,所谓绑定,就是: 
1、设置内核中 inet 相关变量成员的值,以待后用; 
2、协议中,如 TCP协议,记录绑定的协议端口的信息,采用 hash 链表存储,sk 中也同时维护了这么一个链表。两者的区别应该是 前者给协议用。后者给 socket 用。