当套接字创建后,服务端需要调用函数 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
打开文件表里找到对应 fd
的 file
. 在创建 socket
时,sock_map_fd
将 file
和 socket
关联,所以是一一对应关系
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
端口时有哪些注意点呢?换句话说,如果是我们自己实现,会怎么写呢。
- 如果
bind
时不指定端口,那系统会怎么挑选端口? - 如果指定的端口被占用了,系统会不会强制使用?
- 内核如何保存所有
socket
连接?怎样做到高效的冲突检测 -
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_bucket
和 inet_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);
}
- 如果
tb->fastreuseport
或是sk-> sk_reuseport
没有打开,那么肯定不可以重用 - 检查 uid, 必须是同一个用户下的才可以
- 如果当前
tb->fastreuseport
是FASTREUSEPORT_ANY
那么无条件匹配 - 调用
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_bucket
的 socket
集合,也就是说如果想重用这个端口,还要遍历。
- sk != sk2 如果是自己肯定跳过
- 任一一个没有 bindtodeivce ,或是 bound 了同一个需要继续检查,如果分别 bound 不同设备,那肯定不冲突
- tb 连接处于
TCP_LISTEN
状态,并且设置了sk_reuseport_cb
不可以重用 - 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_hash
将 socket
, 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
, 并且多进程监听同一个端口,流量如何分配呢?以后再看吧。
网友评论