分类
linux file system linux network

linux socket REUSEADDR 内核代码分析

第一次接触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参数的代码。我们下街分析。

发表评论

电子邮件地址不会被公开。 必填项已用*标注