系统调用 accept 源码分析

注:本文分析基于 3.10.0-693.el7 内核版本,即 CentOS 7.4

1、函数原型

1
2
3
4
5
6
7
8
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);  

参数说明:
sockfd:套接字的文件描述符,socket()系统调用返回的文件描述符fd
addr:指向存放地址信息的结构体的首地址
addrlen:存放地址信息的结构体的大小,其实也就是sizof(struct sockaddr)

可以看出,bind(),connect(),以及accept()的参数都是一致的。

2、内核实现

1
2
3
4
5
SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen)
{
return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}

最终调用的是 SYSCALL_DEFINE4()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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;

if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;

if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

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

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

newsock->type = sock->type;
newsock->ops = sock->ops;

__module_get(newsock->ops->owner);

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

err = security_socket_accept(sock, newsock);
if (err)
goto out_fd;

//调用inet_accept()执行主处理操作
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;
}

/* File flags are not inherited via accept() unlike another OSes. */
//将文件描述符fd和文件结构体file关联到一起
fd_install(newfd, newfile);
err = newfd;//返回新分配的文件描述符

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

新建 socket 结构体,然后也分配一个 file 结构体,并将这两个结构体相关联,这些操作 socket() 系统调用里也有,包括后面和文件描述符 fd 关联,总的就是将 fd 和文件系统以及网络联系到一起,形成一切皆文件的 Unix 理念。

有了 socket 结构体,那肯定需要有对应的 sock 结构体,这就是 sock->ops->accept() 所做的了。sock->ops 指向 inet_stream_ops

1
2
3
4
5
6
7
8
9
10
11
const struct proto_ops inet_stream_ops = {
.family = PF_INET,
.owner = THIS_MODULE,
.release = inet_release,
.bind = inet_bind,
.connect = inet_stream_connect,
.socketpair = sock_no_socketpair,
.accept = inet_accept,
.getname = inet_getname,
...
};

所以调用的就是 inet_accept()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
struct sock *sk1 = sock->sk;
int err = -EINVAL;
//调用对应协议的accept函数,对于TCP而言,调用的是inet_csk_accept
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)));

//将新分配的socket和接收到的sock相互关联
sock_graft(sk2, newsock);
//返回的新socket状态为已连接
newsock->state = SS_CONNECTED;
err = 0;
release_sock(sk2);
do_err:
return err;
}

然后调用对应协议的 accept 函数,对于TCP协议,

1
2
3
4
5
6
7
8
9
10
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
...
};

因此TCP协议里,对应的 accep t函数就是 inet_csk_accept()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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);

/* 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;

/* Find already established connection */
//如果等待accept的socket队列为空,获取超时时间并等待
if (reqsk_queue_empty(queue)) {
//如果设置的是非阻塞,获取接收数据的超时时间
//可以通过SO_RCVTIMEO选项设置
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;
//等待可accept的请求到来
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
//从全连接accept队列里摘出第一个request_sock处理
req = reqsk_queue_remove(queue);
//将request_sock和sock关联,这样返回后才能继续处理
//其实也就是替代req接管这个请求了
//req->sk就是在第三次握手后服务端新创建的sock
newsk = req->sk;
//sk_ack_backlog计数减1
sk_acceptq_removed(sk);
//快速开启选项打开的情况,
if (sk->sk_protocol == IPPROTO_TCP && queue->fastopenq != NULL) {
spin_lock_bh(&queue->fastopenq->lock);
if (tcp_rsk(req)->listener) {
req->sk = NULL;
req = NULL;
}
spin_unlock_bh(&queue->fastopenq->lock);
}
out:
release_sock(sk);//listen的sock
//请求已被newsk接管,因此这个请求可以释放了
if (req)
__reqsk_free(req);
return newsk;
out_err:
newsk = NULL;
req = NULL;
*err = error;
goto out;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);//listen的sock
//先检查一下再调度出去
if (reqsk_queue_empty(&icsk->icsk_accept_queue))
timeo = schedule_timeout(timeo);//调度出去
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;
}

当有请求到来后,就从全连接队列里取出这个请求,返回请求指向的 sock 结构体,这个结构体也就是在第三次握手中新建的 child sock。然后将这个 sock 结构体和 accept 最开始创建的 socket 结构体关联,通过 sock_graft() 完成关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline void sock_graft(struct sock *sk, struct socket *parent)
{
//sk为全连接队列返回的请求,parent是新分配的socket
write_lock_bh(&sk->sk_callback_lock);
sk->sk_wq = parent->wq;
parent->sk = sk;//新分配的socket的sk指针指向刚接收的请求
sk_set_socket(sk, parent);
security_sock_graft(sk, parent);
write_unlock_bh(&sk->sk_callback_lock);
}

static inline void sk_set_socket(struct sock *sk, struct socket *sock)
{
sk_tx_queue_clear(sk);
//刚接收的请求的sk_socket指针指向刚分配的socket
sk->sk_socket = sock;
}

最后,调用 fd_install() 将 fd 安装到对应 file 结构体中,完成 accept() 的调用。

概括下 accept 的大概流程:

  1. 创建一个 socket 结构体
  2. 获取一个未使用的文件描述符 fd
  3. 创建一个 file 结构体,并和 socket 关联
  4. 从全连接队列中获取客户端发来的请求
  5. 根据请求获取之前新建的 sock 结构体返回
  6. 将请求中的 sock 结构体和开始分配的 socket 结构体关联
  7. 将文件描述符 fd 和文件结构体 file 关联,并返回 fd 供用户使用

Reference


系统调用 accept 源码分析
https://flepeng.github.io/002-Linux-41-系统调用-系统调用-accept-源码分析/
作者
Lepeng
发布于
2024年9月23日
许可协议