linux accept循环等待,accept 如何获取新连接

服务端调用 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 队列?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值