欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Socket之bind、listen实现

程序员文章站 2022-06-29 23:55:06
...

bind()函数的使用方法很简单,但是它是怎么实现的呢?
本文将从应用层出发,沿着网络协议栈,分析了bind()、 listen()的系统调用、Socket层实现,以及它的TCP层实现。

应用层

int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

bind() gives the socket sockfd the local address my_addr.

给socket描述符绑定IP和端口,一般服务器才需要。端口号的范围为0 ~ 65535。调用bind()时,一般不要把端口号置为小于1024的值,因为1到1023是保留端口号。

系统调用

bind()是由glibc提供的,声明位于include/sys/socket.h中,实现位于sysdeps/mach/hurd/bind.c中,主要是用来从用户空间进入名为sys_socketcall的系统调用,并传递参数。sys_scoketcall()实际上是所有socket函数进入内核空间的共同入口。
在sys_socketcall()中会调用sys_bind()。

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    ...
    switch(call) {
        ...
        case SYS_BIND:
            err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
            break;
        ...
    }
   return err;
}

经过了socket层的总入口sys_socketcall(),现在进入sys_bind()。

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    ...
    switch(call) {
        ...
        case SYS_BIND:
            err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
            break;
        ...
    }
   return err;
}

经过了socket层的总入口sys_socketcall(),现在进入sys_bind()。

/*
 * 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;
 
    /* 通过文件描述符fd,找到对应的socket。
     * 以fd为索引从当前进程的文件描述符表files_struct中找到对应的file实例,
     * 然后从file实例的private_data成员中获取socket实例。
     */
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
 
    if (sock) {
        /* 把用户空间的地址复制到内核空间,成功返回0 */
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
 
        if (err >= 0) {
            /* SELInux相关 */
            err = security_socket_bind(sock, (struct sockaddr *)&address, addrlen); 
            if (!err)
                /* socket层的操作函数集。如果是SOCK_STREAM的话,proto_ops是inet_stream_ops,
                 * 接下来调用的是inet_bind()。
                 */
                err = sock->ops->bind(sock, (struct sockaddr *)&address, addrlen); 
        }
        fput_light(sock->file, fput_needed);
    }
    return err;
}

通过文件描述符,找到对应的file结构。

static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)
{
    struct file *file;
    struct socket *sock;
 
    *err = -EBADF; /* Bad file number */
 
    /* 从当前进程的files_struct中找到网络文件系统中的file指针,并增加它的引用计数 */
    file = fget_light(fd, fput_needed);
 
    if (file) {
        sock = sock_from_file(file, err); /* 通过file找到对应的socket */
        if (sock)
            return sock;
        fput_light(file, *fput_needed); /* 失败的话减少file的引用计数 */
    }
    return NULL;
}

通过file结构,找到对应的socket结构。

struct socket *sock_from_file(struct file *file, int *err)
{
    if (file->f_op == &socket_file_ops) /* 说明此file对应一个socket */
        return file->private_data; /* set in sock_map_fd */
 
    *err = -ENOTSOCK;
    return NULL;
}

把用户空间的socket地址复制到内核空间,同时检查是否合法,成功返回0。

int move_addr_to_kernel(void __user *uaddr, int ulen, struct sockaddr_storage *kaddr)
{
    if (ulen < 0 || ulen > sizeof(struct sockaddr_storage)) /* socket地址长度是否合法 */
        return -EINVAL;
 
    if (ulen == 0)
        return 0;
 
    if (copy_from_user(kaddr, uaddr, ulen))
        return -EFAULT; /* socket地址是否合法 */
 
    return audit_sockaddr(ulen, kaddr);
}

socket层

SOCK_STREAM套接口的socket层操作函数集实例为inet_stream_ops,其中绑定函数为inet_bind()。

const struct proto_ops inet_stream_ops = {
    .family = PF_INET,
    .owner = THIS_MODULE,
    ...
    .bind = inet_bind, /* socket层的bind实现 */
    ...
}

socket层做的主要事情为合法性检查、绑定IP地址,而真正的端口绑定是在TCP层进行的。

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); /* INET实例 */
    unsigned short snum; /* 要绑定的端口 */
    int chk_addr_ret; /* IP地址类型 */
    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;
 
    if (addr_len < sizeof(struct sockaddr_in)) /* socket地址长度错误 */
        goto out;
 
    if (addr->sin_family != AF_INET) { /* 非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; /* Cannot assign requested address */
 
    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; /* Permission denied */
    /* snum为0表示让系统随机选择一个未使用的端口,因此是合法的。
    * 如要需要绑定的端口为1 ~ 1023,则需要对应的特权。
    */
    if (snum && snum < PORT_SOCK && ! capable(CAP_NET_BIND_SERVICE))
        goto out;
 
    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;
 
    /* 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.
     */
    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;
}
 
/* Sockets 0 - 1023 can't be bound to unless you are superuser */
#define PORT_SOCK 1024
/* Allows binding to TCP/UDP sockets below 1024 */
#define CAP_NET_BIND_SERVICE 10
 

listen实现

应用层
int listen(int sockfd, int backlog);
Accept incoming connections and a queue limit for incoming connections.

backlog的定义

Now it specifies the queue length for completely established sockets waiting to be accepted,

instead of the number of incomplete connection requests. The maximum length of the queue

for incomplete sockets can be set using the tcp_max_syn_backlog sysctl. When syncookies

are enabled there is no logical maximum length and this sysctl setting is ignored.

全连接队列的最大长度:
backlog保存的是完成三次握手、等待accept的全连接,而不是半连接。
负载不高时,backlog不用太大。(For complete connections)

系统最大的、未处理的全连接数量为:min(backlog, somaxconn),net.core.somaxconn默认为128。

半连接队列的最大长度:
tcp_max_syn_backlog默认值为256。(For incomplete connections)
当使用SYN Cookie时,这个参数变为无效。

半连接队列的最大长度为backlog、somaxconn、tcp_max_syn_backlog的最小值。

系统调用

listen()是由glibc提供的,声明位于include/sys/socket.h中,实现位于sysdeps/mach/hurd/listen.c中,主要是用来从用户空间进入名为sys_socketcall的系统调用,并传递参数。sys_socketcall()实际上是所有socket函数进入内核空间的共同入口。
在sys_socketcall()中会调用sys_listen()。

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
   ...
   switch(call) {
        ...
        case SYS_LISTEN:
            err = sys_listen(a0, a1);
            break;
        ...
    }
    return err;
}

经过了socket层的总入口sys_socketcall(),现在进入sys_listen()。

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    struct socket *sock;
    int err, fput_needed;
    int somaxconn;
 
    /* 通过文件描述符fd,找到对应的socket。
     * 以fd为索引从当前进程的文件描述符表files_struct中找到对应的file实例,
     * 然后从file实例的private_data成员中获取socket实例。
     */
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
 
    if (sock) {
        /* backlog不能超过系统参数somaxconn */
        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
        if ((unsigned int)backlog > somaxconn)
            backlog = somaxconn;
 
        err = security_socket_listen(sock, backlog);
 
        if (! err)
            /* socket层的操作函数,如果是SOCK_STREAM的话,proto_ops是inet_stream_ops,
             * 接下来调用的是inet_listen()。
             */
            err = sock->ops->listen(sock, backlog);
 
        fput_light(sock->file, fput_needed);
    }    
}

socket层

SOCK_STREAM套接口的socket层操作函数集实例为inet_stream_ops,其中监听函数为inet_listen()。

const struct proto_ops inet_stream_ops = {
    .family = PF_INET,
    .owner = THIS_MODULE,
    ...
    .listen = inet_listen, /* socket层的实现 */
    ...
};
/*
 * @sk_ack_backlog: current listen backlog
 * @sk_max_ack_backlog: listen backlog set in listen()
 */
struct sock {
    ...
    unsigned short sk_ack_backlog; /* 当前的backlog,当前全连接队列长度 */
    unsigned short sk_max_ack_backlog; /* 最大的backlog,最大全连接队列长度 */
    ...
};

检查套接口的状态、当前连接的状态是否合法,然后调用inet_csk_listen_start()启动监听。

/* Move a socket into listening state. */
int inet_listen(struct socket *sock, int backlog)
{
    struct sock *sk = sock->sk;
    unsigned char old_state;
    int err;
 
    lock_sock(sk);
    err = -EINVAL;
 
    /* 此时套接口状态需为SS_UNCONNECTED,套接口类型需为SOCK_STREAM */
    if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
        goto out;
 
    old_state = sk->sk_state; /* 当前连接状态 */
 
    /* 当前的连接需为CLOSED或LISTEN状态 */
    if (! ((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
        goto out;
 
    /* Really, if the socket is already in listen state
     * we can only allow the backlog to be adjusted.
     */
    if (old_state != TCP_LISTEN) {
        /* 启动监听 */
        err = inet_csk_listen_start(sk, backlog);
        if (err)
            goto out;
    }
 
    sk->sk_max_ack_backlog = backlog; /* 最大全连接队列长度 */
    err = 0
 
out:
    release_sock(sk);
    return err;
}

启动监听时,做的工作主要包括:

  1. 创建半连接队列的实例,初始化全连接队列。

  2. 初始化sock的一些变量,把它的状态设为TCP_LISTEN。

  3. 检查端口是否可用,防止bind()后其它进程修改了端口信息。

  4. 把sock链接进入监听哈希表listening_hash中。

int inet_csk_listen_start(struct sock *sk, const in nr_table_entries)
{
    struct inet_sock *inet = inet_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);
 
    /* 初始化全连接队列,创建半连接队列的实例 */
    int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
 
    if (rc != 0)
        return rc;
 
    sk->sk_max_ack_backlog = 0; /* 在返回inet_listen()时赋值 */
    sk->sk_ack_backlog = 0;
    inet_csk_delack_init(sk); /* icsk->icsk_ack结构清零 */
 
    sk->sk_state = TCP_LISTEN; /* 把sock的状态置为LISTEN */
 
    /* 检查端口是否仍然可用,防止bind()后其它进程修改了端口信息 */
    if (! sk->sk_prot->get_port(sk, inet->inet_num)) {
        inet->inet_sport = htons(inet->inet_num);
        sk_dst_reset(sk);
        sk->sk_prot->hash(sk); /* 把sock链接入监听哈希表中 */
        return 0;
    }
 
    sk->sk_state = TCP_CLOSE;
    /* 如果端口不可用,则释放半连接队列 */
    __reqsk_queue_destroy(&icsk->icsk_accept_queue);
    return -EADDRINUSE;
}

半连接队列

listen_sock结构用于保存SYN_RECV状态的连接请求块,所以也叫半连接队列。

(1)创建

queue是连接请求控制块,nr_table_entries是半连接的最大个数,即backlog。

int reqsk_queue_alloc(struct request_sock_queue *queue, unsigned int nr_table_entries)
{
    size_t lopt_size = sizeof(struct listen_sock);
    struct listen_sock *lopt;
 
    /* nr_table_entries必需在[8, sysctl_max_syn_backlog]之间,默认是[8, 256] 
     * 但实际上在sys_listen()中要求backlog <= sysctl_somaxconn(默认为128)
     * 所以此时默认区间为[8, 128]
     */
    nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
    nr_table_entries = max_t(u32, nr_table_entries, 8);
 
    /* 使nr_table_entries = 2^n,向上取整 */
    nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
 
    /* 增加一个指针数组的长度 */
    lopt_size += nr_table_entries * sizeof(struct request_sock *);
 
    if (lopt_size > PAGE_SIZE)
        lopt = vzalloc(lopt_size); /* 如果申请内存大于1页,则申请虚拟地址连续的空间 */
    else
        lopt = kzalloc(lopt_size, GFP_KERNEL); /* 申请内存在1页内,则申请物理地址连续的空间 */
 
    /* 相当于把max_qlen_log设置为nr_table_entries以2为底的对数 */
    for (lopt->max_qlen_log = 3, (1 << lopt->max_qlen_log) < nr_table_entries; lopt->max_qlen_log++);
 
    get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd); /* 获取一个随机数 */
 
    rwlock_init(&queue->syn_wait_lock);
    queue->rskq_accept_head = NULL; /* 全连接队列置为空 */
    lopt->nr_table_entries = nr_table_entries; /* 半连接队列的最大长度 */
 
    write_lock_bh(&queue->syn_wait_lock);
    queue->listen_opt = lopt; /* 初始化半连接队列 */
    write_unlock_bh(&queue->syn_wait_lock);
 
    return 0;
}

(2)销毁
销毁连接请求块中的listen_sock实例,释放半连接队列。

void __reqsk_queue_destroy(struct request_sock_queue *queue)
{
    struct listen_sock *lopt;
    size_t lopt_size;
 
    lopt = queue->listen_opt;
    lopt_size = sizeof(struct listen_sock) + lopt->nr_table_entries * 
                         sizeof(struct request_sock *);
 
    if (lopt_size > PAGE_SIZE)
        vfree(lopt);
    else
        kfree(lopt);
}

转载于:https://www.jianshu.com/p/9a193b76610c