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,供用户使用。