第一次接触REUSEADDR是在一年前,那个时候写了一个网络服务程序。写了一个监控脚本监控网络服务,当服务挂了,立马拉起来。但是经常会遇到拉不起来的情况,说端口已经被占用。但是那个端口自有我自己在用。怎么会被占用呢。后来查到说进程挂了,但是内核中连接还在。所以不能立马用。加上REUSEADDR就可以解决问题。今天挖掘一下内核代码。分析一下REUSEADDR实现。
创建socket
int __sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
int flags;
/* Check the SOCK_* constants for consistency. */
BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
retval = sock_create(family, type, protocol, &sock); //根据用户传入的参数选择对应的协议创建对应的socket
if (retval < 0)
return retval;
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); //将socket和文件描述符绑定。
}
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
在linux中一切皆文件,sock_map_fd函数将sock和文件关联起来,返回给用户一个文件描述符。
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
int fd = get_unused_fd_flags(flags);
if (unlikely(fd < 0)) {
sock_release(sock);
return fd;
}
newfile = sock_alloc_file(sock, flags, NULL); //创建文件,
if (likely(!IS_ERR(newfile))) {
fd_install(fd, newfile); //将文件和文件描述符关联
return fd; //返回文件描述符
}
put_unused_fd(fd);
return PTR_ERR(newfile);
}
sock_alloc_file是创建sock匿名文件系统的文件。代码如下
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
struct qstr name = { .name = "" };
struct path path;
struct file *file;
if (dname) {
name.name = dname;
name.len = strlen(name.name);
} else if (sock->sk) {
name.name = sock->sk->sk_prot_creator->name;
name.len = strlen(name.name);
}
path.dentry = d_alloc_pseudo(sock_mnt->mnt_sb, &name);
if (unlikely(!path.dentry)) {
sock_release(sock);
return ERR_PTR(-ENOMEM);
}
path.mnt = mntget(sock_mnt);
d_instantiate(path.dentry, SOCK_INODE(sock));
file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
&socket_file_ops); //创建文件,文件的读写函数是socket_file_ops指定。
if (IS_ERR(file)) {
/* drop dentry, keep inode for a bit */
ihold(d_inode(path.dentry));
path_put(&path);
/* ... and now kill it properly */
sock_release(sock);
return file;
}
sock->file = file;
file->f_flags = O_RDWR | (flags & O_NONBLOCK);
file->private_data = sock;
return file;
}
创建问socket 后使用setsockopt设置REUSEADDR。代码如下
static int __sys_setsockopt(int fd, int level, int optname,
char __user *optval, int optlen)
{
int err, fput_needed;
struct socket *sock;
if (optlen < 0)
return -EINVAL;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock != NULL) {
err = security_socket_setsockopt(sock, level, optname);
if (err)
goto out_put;
if (level == SOL_SOCKET) //第二个参数时这个 调用sock_setsockopt函数
err =
sock_setsockopt(sock, level, optname, optval,
optlen); //调用该函数设置 REUSEADDR。
else
err =
sock->ops->setsockopt(sock, level, optname, optval,
optlen);
out_put:
fput_light(sock->file, fput_needed);
}
return err;
}
SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
char __user *, optval, int, optlen)
{
return __sys_setsockopt(fd, level, optname, optval, optlen);
}
sock_setsockopt函数非常长,因为包含很多参数,所以函数很长,但是逻辑简单,关键代码如下
case SO_REUSEADDR:
sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE); //如果使用REUSEADDR 则sk->sk_reuse置位
break;
最后看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); //最后调用4层协议的bind函数,接下来以udp为例分析
}
fput_light(sock->file, fput_needed);
}
return err;
}
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
return __sys_bind(fd, umyaddr, addrlen);
}
以 IPV4 udp 为例,可以得知 sock->ops->bind最终调用的函数是inet_bind函数,函数在net/ipv4/af_inet.c文件中。inet_bind 经过一些安全检查调用 __inet_bind函数, __inet_bind函数中会调用协议对应的get_port函数,判断绑定的端口是否能够被使用。
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;
}
}
以ipv4 udp为例子,get_port调用的是udp_v4_get_port 文件位置是net/ipv4/udp.c
udp_v4_get_port函数最终调用udp_lib_get_port函数判断地址是否被使用。udp_lib_get_port调用udp_lib_lport_inuse2判断地址是否被使用。
udp_lib_lport_inuse2函数
static int udp_lib_lport_inuse2(struct net *net, __u16 num,
struct udp_hslot *hslot2,
struct sock *sk)
{
struct sock *sk2;
kuid_t uid = sock_i_uid(sk);
int res = 0;
spin_lock(&hslot2->lock);
udp_portaddr_for_each_entry(sk2, &hslot2->head) {
if (net_eq(sock_net(sk2), net) &&
sk2 != sk &&
(udp_sk(sk2)->udp_port_hash == num) &&
(!sk2->sk_reuse || !sk->sk_reuse) && //如果都设置了reuse,则跳过sk2 继续查找下一个。
(!sk2->sk_bound_dev_if || !sk->sk_bound_dev_if ||
sk2->sk_bound_dev_if == sk->sk_bound_dev_if) &&
inet_rcv_saddr_equal(sk, sk2, true)) {
if (sk2->sk_reuseport && sk->sk_reuseport &&
!rcu_access_pointer(sk->sk_reuseport_cb) &&
uid_eq(uid, sock_i_uid(sk2))) {
res = 0;
} else {
res = 1;
}
break;
}
}
spin_unlock(&hslot2->lock);
return res;
}
由上面的代码可以看出,在udp模式下,允许两个socket共享一个地址。而不报错。
进一步分析代码,我们可以发现,只是使用REUSEADDR,虽然在同一个地址上创建多个socket,但是只有最后一个socket是收包的,其他socket不收包,做不到多个socket 负载均衡的功能,要想使用负载均衡的功能需要使用REUSEPORT参数。在上面代码里面已经隐隐约约能看到REUSEPORT参数的代码。我们下街分析。