Linux内核accept系统调用源码分析

内核版本:Linux 3.10

内核源码地址:https://elixir.bootlin.com/linux/v3.10/source (包含各个版本内核源码,且网页可全局搜索函数)

一、应用层-accept()函数

/**
 * sockfd:监听socket的文件描述符
 * addr:存放地址信息的结构体的首地址(用来保存客户端的IP、Port)
 * addrlen:存放地址信息的结构体的大小
 * 成功,返回一个通信socket的文件描述符;失败返回-1
 */
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

通过网络栈专用操作函数集的总入口函数(sys_socketcall函数),请求会分发到sys_accept4()函数。
具体细节可以参考《Linux内核socket系统调用源码分析》

二、内核实现

1.sys_accept4()函数

建立服务器与客户端通信的"桥梁"。
为客户端创建一个socket(后续我们称它为通信socket);
并在网络文件系统中为它申请文件号和文件结构;
最终返回通信socket的文件描述符和客户端地址信息。

// file: net/socket.c
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr, int __user *, upeer_addrlen, int, flags)
{
    struct socket *sock, *newsock;
    struct file *newfile;
    int err, len, newfd, fput_needed;
    struct sockaddr_storage address;

    ......

    // 获取fd对应的socket结构
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;

    err = -ENFILE;
    newsock = sock_alloc(); //分配一个新的socket结构体
    if (!newsock)
        goto out_put;

    newsock->type = sock->type; //继承监听socket结构的类型和函数表
    newsock->ops = sock->ops;

    __module_get(newsock->ops->owner); //目前是空函数

    newfd = get_unused_fd_flags(flags); //获取一个未使用的文件描述符,这个描述符也就是accept()返回的fd
    if (unlikely(newfd < 0)) {
        err = newfd;
        sock_release(newsock);
        goto out_put;
    }
    
    // 创建一个file结构体,同时将这个file结构体和刚刚创建的newsock关联
    newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
    if (unlikely(IS_ERR(newfile))) {
        err = PTR_ERR(newfile);
        put_unused_fd(newfd);
        sock_release(newsock);
        goto out_put;
    }

    err = security_socket_accept(sock, newsock); //SElinux相关,忽略
    if (err)
        goto out_fd;

    // 调用监听socket函数表的接收连接函数
    err = sock->ops->accept(sock, newsock, sock->file->f_flags);
    if (err < 0)
        goto out_fd;

    if (upeer_sockaddr) { //如果要获取对端连接信息,那么拷贝对应信息到用户空间
        // 调用inet_getname()获取对端信息
        if (newsock->ops->getname(newsock, (struct sockaddr *)&address, &len, 2) < 0) {
            err = -ECONNABORTED;
            goto out_fd;
        }
        err = move_addr_to_user(&address, len, upeer_sockaddr, upeer_addrlen); //拷贝对端连接信息到用户空间
        if (err < 0)
            goto out_fd;
    }

    fd_install(newfd, newfile); //将文件描述符fd和文件结构体file关联到一起
    err = newfd; //返回新分配的文件描述符(通信socket文件描述符)

out_put:
    fput_light(sock->file, fput_needed);
out:
    return err;
out_fd:
    fput(newfile);
    put_unused_fd(newfd);
    goto out_put;
}

在服务器socket创建过程中,sock->ops = answer->ops,而answer对应的结构:

// file: net/ipv4/af_inet.c
static struct inet_protosw inetsw_array[] =
{
    {
        .type =       SOCK_STREAM,
        .protocol =   IPPROTO_TCP,
        .prot =       &tcp_prot,
        .ops =        &inet_stream_ops,
        .no_check =   0,
        .flags =      INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK,
    },
    ......
}

所以,sock->ops函数操作表,挂入了inet_stream_ops。具体细节可参考《Linux内核socket系统调用源码分析

// file: net/ipv4/af_inet.c
const struct proto_ops inet_stream_ops = {
    ......
    .accept           = inet_accept,
    ......
};

因此,sock->ops->accept()最终调用的是inet_accept()函数。

2.inet_accept()函数

从全连接队列中,获取一个包含客户端信息的sock,并与新创建的socket关联上。

// file: net/ipv4/af_inet.c
int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
    struct sock *sk1 = sock->sk; //取得监听socket的sock指针
    int err = -EINVAL;
    // 调用对应协议的accept函数
    // 从全连接队列中获取第三次握手时创建的sock
    struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);

    if (!sk2)
        goto do_err;

    lock_sock(sk2);

    sock_rps_record_flow(sk2);
    WARN_ON(!((1 << sk2->sk_state) &
          (TCPF_ESTABLISHED | TCPF_SYN_RECV |
          TCPF_CLOSE_WAIT | TCPF_CLOSE)));

    sock_graft(sk2, newsock); //将新分配的socket和接收到的sock相互关联,形成完整的通信socket

    newsock->state = SS_CONNECTED; //返回的新socket状态设置为已连接(socket、sock这是两个不同结构的状态,我们平时说的TCP状态是sock结构的状态)
    err = 0;
    release_sock(sk2);
do_err:
    return err;
}

在服务器socket创建过程中,sk1->sk_prot挂入的是tcp_prot结构:

// file: net/ipv4/tcp_ipv4.c
struct proto tcp_prot = {
    ......
    .accept            = inet_csk_accept,
    ......
};

因此,sk1->sk_prot->accept()最终调用的是inet_csk_accept()函数。

3.inet_csk_accept()函数

从全连接队列中获取一个通信sock

// file: net/ipv4/inet_connection_sock.c
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
    struct sock *newsk;
    struct request_sock *req;
    int error;

    lock_sock(sk);
    error = -EINVAL;
    if (sk->sk_state != TCP_LISTEN) //接受连接之前必须处于监听状态
        goto out_err;

    // 检查全连接队列
    if (reqsk_queue_empty(queue)) { //全连接队列为空
        long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); //确定睡眠时间

        error = -EAGAIN;
        if (!timeo) //非阻塞的socket,无需睡眠
            goto out_err;

        //进入循环睡眠等待连接到来
        //与第三次握手,建立连接完成时,唤醒服务器程序接收连接,对接上了
        error = inet_csk_wait_for_connect(sk, timeo); //跟客户端第一次握手等待连接类似
        if (error)
            goto out_err;
    }
    req = reqsk_queue_remove(queue); //从全连接队列中获取一个通信sock
    newsk = req->sk;

    sk_acceptq_removed(sk); //更新全连接队列计数器
    ......
}

在inet_csk_wait_for_connect()函数中,如果此时尚未有客户端发起连接,那就睡眠直到有请求到来,或者如果用户设置了超时时间,也会超时返回。另外,也有可能被信号打断。

三、总结

accept的主要工作就是从全连接队列中获取一个连接,创建通信socket,供用户使用。

posted @ 2024-01-21 10:40  划水的猫  阅读(281)  评论(0编辑  收藏  举报