注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4
1、函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd:套接字的文件描述符,socket()系统调用返回的fd
addr:指向存放地址信息的结构体的首地址
addrlen:存放地址信息的结构体的大小,其实也就是sizeof(struct sockaddr)
2、struct sockaddr结构体
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
sa_family表示协议族,和socket()系统调用的domain保持一致,IPV4对应AF_INET,IPV6对应AF_INET6。因此sa_data也对应两个不同协议。对于AF_INET则使用struct sockaddr_in,因此bind()函数使用时需要将struct sockaddr_in指针强制转换为struct sockaddr *。如下,
struct sockaddr_in server_addr;
bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
同时,addrlen必须设置为sizeof(struct sockaddr_in);AF_INET6则使用struct sockaddr_in6。下面我们以IPV4为例,简单介绍。
3、struct sockaddr_in结构体
/* Structure describing an Internet (IP) socket address. */
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
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)];
};
#define sin_zero __pad /* for BSD UNIX comp. -FvK */
这个结构体里面的几个变量正是我们编程时用到的,如下,
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;//设置协议族
server_addr.sin_port = htons(PORT);//源端口
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//源地址
了解完这些结构体间的关系,下面我们就来看下bind的内核具体实现。
4、内核实现
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
//根据fd获取相应的socket结构体,这是socket系统调用后的便利之处
//后续listen、connect调用都是如此操作
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;
}
在前面socket()系统调用的时候我们知道sock->ops指向inet_stream_ops,
onst struct proto_ops inet_stream_ops = {
.family = PF_INET,
.owner = THIS_MODULE,
.release = inet_release,
.bind = inet_bind,
.connect = inet_stream_connect,
.socketpair = sock_no_socketpair,
.accept = inet_accept,
.getname = inet_getname,
.poll = tcp_poll,
.ioctl = inet_ioctl,
.listen = inet_listen,
.shutdown = inet_shutdown,
.setsockopt = sock_common_setsockopt,
.getsockopt = sock_common_getsockopt,
.sendmsg = inet_sendmsg,
.recvmsg = inet_recvmsg,
.mmap = sock_no_mmap,
.sendpage = inet_sendpage,
.splice_read = tcp_splice_read,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_sock_common_setsockopt,
.compat_getsockopt = compat_sock_common_getsockopt,
.compat_ioctl = inet_compat_ioctl,
#endif
};
所以sock->ops->bind指向的便是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;
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
unsigned short snum;
int chk_addr_ret;
int err;
//如果sk->sk_prot有自己的bind函数那就调用自己的bind函数
//这个只有RAW类型协议才有设置自己的bind函数,这里讨论TCP,因此不涉及
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;
if (addr->sin_family != AF_INET) {//检查地址族,IPV4对应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;
}
//判断源地址的类型,广播,多播还是单播
chk_addr_ret = inet_addr_type(net, addr->sin_addr.s_addr);
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;
//如果指定了源端口,检查源端口设置是否合法
//因为非root用户不能使用小于1024的端口号,PROT_SOCK的值为1024
if (snum && snum < PROT_SOCK &&
!ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
goto out;
lock_sock(sk);
/* Check these errors (active socket, double bind). */
err = -EINVAL;
//close状态的socket不能再绑定端口和ip了
//同时如果已经绑定过端口了的socket也不允许重新绑定
//inet->inet_num表示的就是该socket最终通信使用的端口号
if (sk->sk_state != TCP_CLOSE || inet->inet_num)
goto out_release_sock;
//设置源ip地址
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 */
//用户设置了绑定的端口
//或者没有设置bind不绑定端口的选项(等到connect的时候再选择端口)
if ((snum || !inet->bind_address_no_port) &&
//同时获取端口失败了(一般是端口被占用)
sk->sk_prot->get_port(sk, snum)) {
//绑定失败,绑定的地址已经有人在使用
inet->inet_saddr = inet->inet_rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
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;
}
由socket()系统调用可知,sk->sk_prot指向tcp_prot,
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
...
.get_port = inet_csk_get_port,
...
};
所以获取端口sk->sk_prot->get_port调用的便是inet_csk_get_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 inet_bind_bucket *tb;
//attempts变量表示用户未设置具体端口
//使用INADDR_ANY参数让系统自己选择,系统选择端口的尝试次数
int ret, attempts = 5;
struct net *net = sock_net(sk);
//该变量用于记录当端口都有进程使用时,共享进程最少的那个端口号的使用者个数
int smallest_size = -1, smallest_rover;
kuid_t uid = sock_i_uid(sk);
//如果用户设置了socket地址可重用,先只用上半部分端口范围来选择
int attempt_half = (sk->sk_reuse == SK_CAN_REUSE) ? 1 : 0;
local_bh_disable();
//端口号为空,也就是用户没有具体指定端口号,使用INADDR_ANY参数
if (!snum) {
int remaining, rover, low, high;
again:
//获取端口可用范围
//可以通过/proc/sys/net/ipv4/ip_local_port_range设置端口可用范围
inet_get_local_port_range(net, &low, &high);
//地址可重用,选用上半部分端口范围来选择
if (attempt_half) {
int half = low + ((high - low) >> 1);
if (attempt_half == 1)
high = half;
else
low = half;
}
remaining = (high - low) + 1;
//系统随机选择端口号
smallest_rover = rover = prandom_u32() % remaining + low;
smallest_size = -1;
do {
//判断是否是用户保留的端口号
//可以通过/proc/sys/net/ipv4/ip_local_reserved_ports设置
if (inet_is_reserved_local_port(rover))
goto next_nolock;
//获取该端口的bind哈希表,查看是否和现有的连接冲突
head = &hashinfo->bhash[inet_bhashfn(net, rover,
hashinfo->bhash_size)];
spin_lock(&head->lock);
//遍历使用该端口的连接
inet_bind_bucket_for_each(tb, &head->chain)
//如果在同一个网络命名空间里端口号相同,那就冲突了
//需要进一步判断是否可以复用
if (net_eq(ib_net(tb), net) && tb->port == rover) {
//如果表中的fastreuse大于零(该值可理解为地址是否可重用)
//同时当前连接也设置了地址可重用,同时当前连接不处于listen转态
if (((tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
//或者如果表中的fastreuseport大于零(该值可理解为端口是否可重用)
//同时当前连接也设置了端口可重用,且这两个连接的用户是同一个(防止端口窃听)
(tb->fastreuseport > 0 && sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
//同时共享该端口的用户少于之前保存的端口,或者这是第一次选择端口
(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;//-------------(1)
goto tb_found;
}
}
//这里又一次判断系统选择的端口是否冲突
//也就是说,即使不可重用,但是由bind_conflict判断不冲突的,也还是可以使用
//比如两个连接绑定的IP不一样
if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
snum = rover;//----------------(2)
goto tb_found;
}
goto next;
}
//找到了没被使用的端口,跳出循环
break;
next:
spin_unlock(&head->lock);
next_nolock:
//当前端口不能用,下一个端口
if (++rover > high)
rover = low;
} while (--remaining > 0);
ret = 1;
//端口范围内都找了一遍,没找到可以使用的端口
if (remaining <= 0) {
//如果之前又找到过可重用端口,但是被bind_conflict否决的端口
if (smallest_size != -1) {
//使用这个端口
snum = smallest_rover;
goto have_snum;//----------------(3)
}
if (attempt_half == 1) {
//使用下半部分端口范围继续找端口
attempt_half = 2;
goto again;
}
goto fail;
}
/* OK, here is the one we will use. HEAD is
* non-NULL and we hold it's mutex.
*/
snum = rover;//这里就是我们通过系统自动选择得到的端口号
} else {
//这里一般就是说用户指定了绑定的端口的流程,或者上面(3)走到这
have_snum:
//一样,先获取该端口的bind哈希表
head = &hashinfo->bhash[inet_bhashfn(net, snum, hashinfo->bhash_size)];
spin_lock(&head->lock);
//遍历使用该端口的连接
inet_bind_bucket_for_each(tb, &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;
//如果表中的连接和当前连接都允许地址重用,且当前连接不处于listen转态,OK,success
//看到这里有点疑惑,我觉得在(1)里就应该直接走到success
//因为走到(1)里的条件,不是满足下面这个判断,就是满足else分支的判断
//经过relax=false的检测,肯定能通过relax=true的检测
//唯一能解释的就是为了对fastreuse和fastreuseport进行重新确定赋值
if (((tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
//或者如果表中的连接和当前连接都允许端口重用,且这两个连接的用户是同一个
//同时smallest_size为初始值,从这个条件可以看出,这是用户指定端口的流程
//其他(1)(2)都不满足smallest_size为初始值的条件
(tb->fastreuseport > 0 && sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
smallest_size == -1) {
goto success;//端口可用
} else {
ret = 1;
//使用relax=true判断,不冲突的话就可以复用
if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) {
//冲突的话,如果当前连接允许地址可重用,同时当前连接不处于listen转态
if (((sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
//或者表中连接和当前连接都允许端口可重用,且这两个连接的用户是同一个
//并且smallest_size不为初始值,同时尝试次数还没到上限
(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;
//如果是非共用的端口
//创建一个新的inet_bind_bucket,存储这个IP和端口绑定的信息
if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
net, head, snum)) == NULL)
goto fail_unlock;
if (hlist_empty(&tb->owners)) {//如果第一次使用这个端口
//如果当前连接允许地址可重用,且连接状态不为listen
if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
//设置改bind块地址可重用标志为1
tb->fastreuse = 1;
else
tb->fastreuse = 0;
//如果当前连接允许端口可重用
if (sk->sk_reuseport) {
//设置改bind块端口可重用标志为1
tb->fastreuseport = 1;
tb->fastuid = uid;
} else
tb->fastreuseport = 0;
} else {//使用共用的端口
//如果哈希表中的连接允许地址可重用
//但是当前连接不允许地址重用,或者处于listen状态
if (tb->fastreuse &&
(!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
//修改bind块地址可重用标志为0
tb->fastreuse = 0;
//如果哈希表中的连接允许端口可重用
//但是当前连接不允许端口重用或者两个连接不是同一个用户
if (tb->fastreuseport &&
(!sk->sk_reuseport || !uid_eq(tb->fastuid, uid)))
//修改bind块端口可重用标志为0
tb->fastreuseport = 0;
}
success:
if (!inet_csk(sk)->icsk_bind_hash)
//把当前连接加入tb->owners哈希链表中,同时更新bind块的统计信息
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;
}
其中判断绑定端口是否冲突通过icsk_af_ops->bind_conflict()实现,icsk_af_ops是在socket()调用中tcp_v4_init_sock()函数里赋值的,指向ipv4_specific,
onst struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.sk_rx_dst_set = inet_sk_rx_dst_set,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.bind_conflict = inet_csk_bind_conflict,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_ip_setsockopt,
.compat_getsockopt = compat_ip_getsockopt,
#endif
.mtu_reduced = tcp_v4_mtu_reduced,
};
因此是通过inet_csk_bind_conflict()判断,
int inet_csk_bind_conflict(const struct sock *sk,
const struct inet_bind_bucket *tb, bool relax)
{
struct sock *sk2;
int reuse = sk->sk_reuse;//地址可重用标志
int reuseport = sk->sk_reuseport;//端口可重用标志
kuid_t uid = sock_i_uid((struct sock *)sk);//获取该连接的用户id
sk_for_each_bound(sk2, &tb->owners) {
//一开始不是检查过不允许重复绑定吗,为什么还要判断两个连接是否是同一个?
if (sk != sk2 && !inet_v6_ipv6only(sk2) &&
//当前连接没有使用SO_BINDTODEVICE选项绑定网络设备,或者表中的连接没有绑定连接
(!sk->sk_bound_dev_if || !sk2->sk_bound_dev_if ||
//或者两个连接绑定了同一个网络设备
sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
/* 当前连接不允许地址重用或者表中连接不允许地址重用或者表中连接状态为listen
* 同时(当前连接不允许端口重用或者表中连接不允许端口重用
* 或者(表中连接不处于time_wait状态或两个连接不属于同一个用户)) */
if ((!reuse || !sk2->sk_reuse || sk2->sk_state == TCP_LISTEN) &&
(!reuseport || !sk2->sk_reuseport || (sk2->sk_state != TCP_TIME_WAIT &&
!uid_eq(uid, sock_i_uid(sk2))))) {
//表中连接或者当前连接没有绑定ip,或者绑定的ip相同
if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr ||
sk2->sk_rcv_saddr == sk->sk_rcv_saddr)
break;//冲突了
}
//relax为false,且当前连接和表中连接都允许地址重用且表中连接状态不为listen
if (!relax && reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) {
//表中连接或者当前连接没有绑定ip,或者绑定的ip相同
if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr ||
sk2->sk_rcv_saddr == sk->sk_rcv_saddr)
break;//冲突了
}
}
}
//sk2不为空说明就冲突了,返回1;为空说明不冲突,可用,返回0
return sk2 != NULL;
}
这两个函数写的逻辑比较杂乱,我们来直接看下到底哪些情况端口能重用就好了。
1. 当前连接和已绑定的连接绑定不同网络设备
2. 当前连接和已绑定的连接绑定的IP地址不同
3. 当前连接和已绑定的连接都允许地址重用,且已绑定的连接不处于listen状态
4. 当前连接和已绑定的连接都允许端口重用,且已绑定的连接处于time_wait状态,且两个连接属于同一个用户
因此我觉得与其那么复杂的去判断哪些情况不能重用,不如直接判断可重用的情况,其余就是不能重用,这样逻辑简单,容易理解多了,不知道内核为什么这么设计,看起来十分令人费解。
言归正传,如果选择的端口是没有人使用的,那么就要创建一个bind块来记录这个端口和IP信息,通过inet_bind_bucket_create()实现。
struct inet_bind_bucket *inet_bind_bucket_create(struct kmem_cache *cachep,
struct net *net,
struct inet_bind_hashbucket *head,
const unsigned short snum)
{
//创建一个新的inet_bind_bucket
struct inet_bind_bucket *tb = kmem_cache_alloc(cachep, GFP_ATOMIC);
if (tb != NULL) {
write_pnet(&tb->ib_net, net);//保存网络命名空间
tb->port = snum;//绑定的端口
tb->fastreuse = 0;//初始化地址可重用标志
tb->fastreuseport = 0;//初始化端口可重用标志
tb->num_owners = 0;//初始化端口共用人数
INIT_HLIST_HEAD(&tb->owners);
hlist_add_head(&tb->node, &head->chain);//加入到bind哈希表
}
return tb;
}
经过一系列操作,当前选择的端口和其绑定的IP信息都加入到了全局的bind哈希表,后面查询就能查询到了。
最后再将这个bind块和当前连接sock关联在一起。
void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb,
const unsigned short snum)
{
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
atomic_inc(&hashinfo->bsockets);//增加端口的总使用次数
inet_sk(sk)->inet_num = snum;//设置连接最终使用的源端口
sk_add_bind_node(sk, &tb->owners);//把该连接加入tb->owners哈希链表中
tb->num_owners++;//增加该端口共享的人数
inet_csk(sk)->icsk_bind_hash = tb;//将该socket和bind_bucket联系在一起
}
最后我们稍微总结一下bind()的操作。其主要任务就是将IP和端口绑定,然后要考虑这个端口是否能重用,得到端口后将这个绑定信息加入到bind哈希表中。就是这么简单的逻辑。