Socket bind系统调用简要分析
主要查看linux kernel 源码:Socket.c 以及af_inet.c文件
1.1 bind分析
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中的参数解释如下。
·sockfd :表示要绑定地址的套接字描述符。
·addr :表示绑定到套接字的地址。
·addrlen :表示绑定的地址长度。
返回值 0 表示成功, -1 则表示错误
因为 Linux 的套接字是针对多种协议族的,而每个协议族都可以有不同的地址类型。所以 Linux 套接字关于地址的系统调用,统一使用了一个公共结构体,并要求
调用者将实际地址参数进行强制类型转换,以此来避免编译警告
/* * Bind a name to a socket. Nothing much to do here since it\'s * the protocol\'s responsibility to handle the local address. * * We move the socket address to kernel space before we call * the protocol layer (having also checked the address is ok). */ SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen) { struct socket *sock; struct sockaddr_storage address; int err, fput_needed; //通过socket文件符fd找到socket sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { //将地址信息由用户态copy到内核 err = move_addr_to_kernel(umyaddr, addrlen, &address); if (err >= 0) { err = security_socket_bind(sock, (struct sockaddr *)&address, addrlen); if (!err) //调用TCP对应的插口函数执行绑定功能 err = sock->ops->bind(sock, (struct sockaddr *) &address, addrlen); } fput_light(sock->file, fput_needed); } return err; }
如果sock->ops指向inet_stream_ops,那么sock->ops->bind就指向inet_bind:
sock->ops指向inet_dgram_ops,那么sock->ops->bind就指向inet_bind:
//SOCK_STREAM套接口的socket层操作函数集实例为inet_stream_ops,其中绑定函数为inet_bind()。 int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len) { struct sockaddr_in *addr = (struct sockaddr_in *)uaddr; struct sock *sk = sock->sk;/* BSD socket 实例 */ struct inet_sock *inet = inet_sk(sk);/* INET实例 */ unsigned short snum; int chk_addr_ret; int err; /* If the socket has its own bind function then use it. (RAW) 用于原始套接字,TCP协议实例tcp_prot不含此函数指针 */ if (sk->sk_prot->bind) { err = sk->sk_prot->bind(sk, uaddr, addr_len); goto out; } err = -EINVAL;//地址类型必须是struct sockaddr_in if (addr_len < sizeof(struct sockaddr_in)) goto out; if (addr->sin_family != AF_INET) {//地址族必须是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; } /* 在路由中检查IP地址类型,单播、多播还是广播 */ chk_addr_ret = inet_addr_type(sock_net(sk), addr->sin_addr.s_addr); /* 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) */ /* sysctl_ip_nonlocal_bind表示是否允许绑定非本地的IP地址。 * inet->freebind表示是否允许绑定非主机地址。 * 这里需要允许绑定非本地地址,除非是发送给自己、多播或广播。 */ err = -EADDRNOTAVAIL; if (!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; /* snum为0表示让系统随机选择一个未使用的端口,因此是合法的。 * 如要需要绑定的端口为1 ~ 1023,则需要对应的特权。 */ 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); /* Check these errors (active socket, double bind). * 如果套接字不在初始状态TCP_CLOSE,或者已经绑定端口了,则出错。 * 一个socket最多可以绑定一个端口,而一个端口则可能被多个socket共用。 */ 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 */ /* Make sure we are allowed to bind here. * 如果使用的是TCP,则sk_prot为tcp_prot,get_port为inet_csk_get_port() * 端口可用的话返回0。 */ if (sk->sk_prot->get_port(sk, snum)) { inet->inet_saddr = inet->inet_rcv_saddr = 0; err = -EADDRINUSE; goto out_release_sock; } /* inet_rcv_saddr表示绑定的地址,接收数据时用于查找socket */ 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: release_sock(sk); out: return err; }
SOCK_STREAM套接口的TCP层操作函数集实例为tcp_prot,其中端口绑定函数为inet_csk_get_port()
/* Obtain a reference to a local port for the given sock, * if snum is zero it means select any available local port. */ 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 hlist_node *node; struct inet_bind_bucket *tb; int ret, attempts = 5; struct net *net = sock_net(sk); int smallest_size = -1, smallest_rover; local_bh_disable();/* 禁止下半部,防止 进程 软中断抢占 */ /* 如果snum为0,系统自动为sock选择一个端口号 */ if (!snum) { int remaining, rover, low, high; again: inet_get_local_port_range(&low, &high); /* 获取端口号的取值范围 */ remaining = (high - low) + 1;/* 取值范围内端口号的个数 */ smallest_rover = rover = net_random() % remaining + low;/* 随机选取范围内的一个端口 */ smallest_size = -1; do { /* 查看端口是否属于保留的 */ if (inet_is_reserved_local_port(rover)) goto next_nolock;/* rover加1,继续 */ /* 根据端口号,确定所在的哈希桶 */ head = &hashinfo->bhash[inet_bhashfn(net, rover, hashinfo->bhash_size)]; spin_lock(&head->lock); inet_bind_bucket_for_each(tb, node, &head->chain)/* 从头遍历哈希桶 */ if (net_eq(ib_net(tb), net) && tb->port == rover) {/* 如果端口被使用了 */ if (tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN && (tb->num_owners < smallest_size || smallest_size == -1)) { smallest_size = tb->num_owners; smallest_rover = rover; if (atomic_read(&hashinfo->bsockets) > (high - low) + 1 && !inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) { snum = smallest_rover; goto tb_found; } }/* 检查是否有端口绑定冲突,该端口是否能重用 */ if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) { snum = rover; goto tb_found; } goto next;/* 此端口不可重用,看下一个 */ } break;/* 找到了没被用的端口,退出 */ next: spin_unlock(&head->lock); next_nolock: if (++rover > high) rover = low; } while (--remaining > 0); /* Exhausted local port range during search? It is not * possible for us to be holding one of the bind hash * locks if this test triggers, because if \'remaining\' * drops to zero, we broke out of the do/while loop at * the top level, not from the \'break;\' statement. */ ret = 1; if (remaining <= 0) { if (smallest_size != -1) { snum = smallest_rover; goto have_snum; } goto fail; } /* OK, here is the one we will use. HEAD is * non-NULL and we hold it\'s mutex. */ snum = rover; /* 自动选择的可用端口 */ } else { have_snum: head = &hashinfo->bhash[inet_bhashfn(net, snum, hashinfo->bhash_size)]; spin_lock(&head->lock); inet_bind_bucket_for_each(tb, node, &head->chain) 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)) { if (sk->sk_reuse == SK_FORCE_REUSE) goto success; if (tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN && 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 && smallest_size != -1 && --attempts >= 0) { spin_unlock(&head->lock); goto again; } goto fail_unlock; } } } tb_not_found: ret = 1; if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep, net, head, snum)) == NULL) goto fail_unlock; if (hlist_empty(&tb->owners)) {/* 端口上有绑定sock时 */ 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 (!inet_csk(sk)->icsk_bind_hash) inet_bind_hash(sk, tb, snum); WARN_ON(inet_csk(sk)->icsk_bind_hash != tb); ret = 0; fail_unlock: spin_unlock(&head->lock); fail: local_bh_enable(); return ret; } 我们可以指定系统自动分配端口号时,端口的区间: /proc/sys/net/ipv4/ip_local_port_range,默认为:32768 61000 也可以指定要保留的端口区间: /proc/sys/net/ipv4/ip_local_reserved_ports,默认为空 端口绑定冲突 面向连接的、传输层的协议族相关的操作函数集: /* * Pointers to address related TCP functions * (i.e. things that depend on the address family) */ struct inet_connection_sock_af_ops { ... int (*bind_conflict) (const struct sock *sk, const struct inet_bind_bucket *tb, bool relax); ... }; const struct inet_connection_sock_af_ops ipv4_specific = { ... .bind_conflict = inet_csk_bind_conflict, /* 用于判断绑定端口是否冲突 */ ... }; int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax) { struct sock *sk2; struct hlist_node *node; int reuse = sk->sk_reuse; /* * 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. * 遍历此端口上的sock。 */ sk_for_each_bound(sk2, node, &tb->owners) { /* 冲突的条件1:不是同一socket、绑定在相同的设备上 */ if (sk != sk2 && !inet_v6_ipv6only(sk2) && (!sk->sk_bound_dev_if || !sk2->sk_bound_dev_if || sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) { /* 冲突的条件2:绑定在相同的IP上 * 冲突的条件3(符合一个即满足): * 3.1 本socket不允许重用 * 3.2 链表中的socket不允许重用 * 3.3 链表中的socket处于监听状态 */ if (!reuse || !sk2->sk_reuse || sk2->sk_state == TCP_LISTEN) { const __be32 sk2_rcv_saddr = sk_rcv_saddr(sk2); if (!sk2_rcv_saddr || !sk_rcv_saddr(sk) || sk2_rcv_saddr == sk_rcv_saddr(sk)) break; } if (!relax && reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) { const __be32 sk2_rcv_saddr = sk_rcv_saddr(sk2); if (!sk2_rcv_saddr || !sk_rcv_saddr(sk) || sk2_rcv_saddr == sk_rcv_saddr(sk)) break; } } } return node != NULL; }
Q: 什么情况下会出现冲突呢?
A: 同时符合以下条件才会冲突:
1. 绑定的设备相同(不允许自动选择设备)
2. 绑定的IP地址相同(不允许自动选择IP)
3 以下条件有一个成立:
3.1 要绑定的socket不允许重用
3.2 已绑定的socket不允许重用
3.3 已绑定的socket处于监听状态
3.4 relax参数为false
我们看到系统自动选择端口时,relax为false,是不允许这种情况的。
tcp: bind() fix autoselection to share ports
The current code checks for conflicts when the application requests a specific port.
If there is no conflict, then the request is granted.
On the other hand, the port autoselection done by the kernel fails when all ports are bound
even when there is a port whith no conflict available.
The fix changes port autoselection to check if there is a conflict and use it if not.
作者的意思是,在系统自动选择端口时,判断可重用端口的主要条件为:
tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN
而其实不符合此条件、但通过bind_conflict()检查的端口也是可以重用的
简单的把上面那几行代码就这样加进去,系统自动选择端口的思路就变为:
1. 随机选取一个端口。
2. 检查其是否被使用了。
2.1 没有被使用,那么就是这个端口了,退出:)
2.2 被使用了,检查重用是否有冲突。
2.2.1 没有冲突,就重用这个端口,退出!
2.2.2 有冲突,继续遍历。
3. 端口++,重复1和2。
linux 内核后面 增加了SO_REUSEPORT
, 并且多进程监听同一个端口,这个 也要考虑呗,
这个是为了解决惊群问题,进群的问题 有很多方案 为什么要 选择这一种 ,后面在一一细说