服务端调用 listen
后开始监听,然后死循环调用 accept
接收新的连接请求,将新连接 fd
加到 epoll
等待事件。对于客户端,只需要 connect
即可。linux 如何实现的呢?
函数声明
先看 man
说明,关于 accept
有两个版本,新的 accept4
额外多了一个 flags
参数,可以设置 SOCK_NONBLOCK
, SOCK_CLOEXEC
. 其实底层都是调用一个 accept4
.
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags);
返回值是新连接的 fd
, 参数 addr
如果不为空,则被填充为新连接的地址信息。
源码实现
int __sys_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;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
flags
仅支持 SOCK_CLOEXEC
和 SOCK_NONBLOCK
, 设置其它位无效报错。
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
根据 fd
查找到当前监听 socket
err = -ENFILE;
newsock = sock_alloc();
if (!newsock)
goto out_put;
newsock->type = sock->type;
newsock->ops = sock->ops;
sock_alloc
分配新的 socket
结构体,用于建立新连接
/*
* We don't need try_module_get here, as the listening socket (sock)
* has the protocol module (sock->ops->owner) held.
*/
__module_get(newsock->ops->owner);
newfd = get_unused_fd_flags(flags);
if (unlikely(newfd < 0)) {
err = newfd;
sock_release(newsock);
goto out_put;
}
newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
if (IS_ERR(newfile)) {
err = PTR_ERR(newfile);
put_unused_fd(newfd);
goto out_put;
}
由于 linux 一切皆文件,所以新的 socket
连接,必然有 fd
, file
err = security_socket_accept(sock, newsock);
if (err)
goto out_fd;
err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
if (err < 0)
goto out_fd;
accept
回调 inet_stream_ops.inet_accept
, 而 inet_accept
最终调用 tcp_prot.inet_csk_accept
核心函数。
if (upeer_sockaddr) {
len = newsock->ops->getname(newsock,
(struct sockaddr *)&address, 2);
if (len < 0) {
err = -ECONNABORTED;
goto out_fd;
}
err = move_addr_to_user(&address,
len, upeer_sockaddr, upeer_addrlen);
if (err < 0)
goto out_fd;
}
获取新连接的地址信息,如果调用方参数 addr
不为空,则填充。
/* File flags are not inherited via accept() unlike another OSes. */
fd_install(newfd, newfile);
err = newfd;
fd_install
将 fd
, file
关联,并添加到当前进程 PCB
打开文件表。
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
out_fd:
fput(newfile);
put_unused_fd(newfd);
goto out_put;
}
inet_csk_accept实现
这是核心代码,大致流程是从队列取待建连的 socket
, 队列如果为空,那么跟据是否设置 O_NONBLOCK
判断是否等待。
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
int error;
icsk_accept_queue
在 listen
时初始化,存放处于 SYN_RECV
状态等待建连的 socket
,也就是说 inet_csk_accept
是消费者。那么生产者是内核的哪个模块呢?先挖大坑
lock_sock(sk);
/* We need to make sure that this socket is listening,
* and that it has something pending.
*/
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
goto out_err;
当前状态必须处于 TCP_LISTEN
/* Find already established connection */
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo)
goto out_err;
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
如果设置了 O_NONBLOCK
非阻塞,队列没有数据直接返回。否则 inet_csk_wait_for_connect
等待新连接到来,并一直阻塞在这里,直到 timeo
超时。
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
如果逻辑走到这里,那么 icsk_accept_queue
队列一定有请求到来
if (sk->sk_protocol == IPPROTO_TCP &&
tcp_rsk(req)->tfo_listener) {
spin_lock_bh(&queue->fastopenq.lock);
if (tcp_rsk(req)->tfo_listener) {
/* We are still waiting for the final ACK from 3WHS
* so can't free req now. Instead, we set req->sk to
* NULL to signify that the child socket is taken
* so reqsk_fastopen_remove() will free the req
* when 3WHS finishes (or is aborted).
*/
req->sk = NULL;
req = NULL;
}
spin_unlock_bh(&queue->fastopenq.lock);
}
这里涉及 TFO
,暂时忽略
out:
release_sock(sk);
if (req)
reqsk_put(req);
return newsk;
out_err:
newsk = NULL;
req = NULL;
*err = error;
goto out;
}
inet_csk_wait_for_connect实现
这个函数不长,但是涉及内核调度,超时。
static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{
struct inet_connection_sock *icsk = inet_csk(sk);
DEFINE_WAIT(wait);
int err;
for (;;) {
prepare_to_wait_exclusive(sk_sleep(sk), &wait,
TASK_INTERRUPTIBLE);
release_sock(sk);
if (reqsk_queue_empty(&icsk->icsk_accept_queue))
timeo = schedule_timeout(timeo);
sched_annotate_sleep();
lock_sock(sk);
err = 0;
if (!reqsk_queue_empty(&icsk->icsk_accept_queue))
break;
err = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
break;
err = sock_intr_errno(timeo);
if (signal_pending(current))
break;
err = -EAGAIN;
if (!timeo)
break;
}
finish_wait(sk_sleep(sk), &wait);
return err;
}
1.首先定义 wait
, 这是一个 wait_queue_entry
结构体,DEFINE_WAIT
是宏,看下展开式:
#define DEFINE_WAIT_FUNC(name, function) \
struct wait_queue_entry name = { \
.private = current, \
.func = function, \
.entry = LIST_HEAD_INIT((name).entry), \
}
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
这块很好理解,current
是当前进程,定义了一个 wait entry
2.每个 struct sock
有一个 socket_wq
成员,等待 socket
事件的进程会放到 socket_wq.wait_queue_head_t
链表中,等待唤醒。sk_sleep(sk)
获取链表,prepare_to_wait_exclusive
将当前进程注册到这个链表。
3.schedule_timeout
注册超时事件,出让 cpu, 将控制权交给内核,此时程序阻塞在这里。
4.程序被唤醒后,检查 icsk_accept_queue
队列是否有请求,没有的话,并且 timeo
还未到期,循环继续调度 prepare_to_wait_exclusive
5.如果此时队列有数据,或是超时到期,那么返回错误码。
小结
accept
至此大致看完。挖了一个大坑,当有客户端发起 SYN
请求,内核是如何处理,并写入 icsk_accept_queue
队列?