目录
前言
从监听套接字的全连接队列中取出一个已完成三次握手的连接,返回其对应的套接字;如果队列为空,阻塞等待或非阻塞模式下立即返回-EAGAIN,看似简单的过程也隐藏着一些细节:
- 监听 socket 和 accept 返回的新 socket 有什么区别?
- 阻塞与非阻塞模式下 accept 行为有什么差异?
- accept 参与三次握手吗?
一、用户态 accept() 基础
1.1 函数原型
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数详解:
- sockfd:监听套接字的文件描述符
- addr:传出参数,存储客户端IP和端口
- addrlen:传入传出参数, 传入 addr 缓冲区大小,调用结束后返回实际地址结构体长度
返回值:成功返回一个新的套接字文件描述符;失败返回-1
重要区分:
- sockfd: 监听 socket,专门用来接收新连接,不负责收发业务数据
- 返回的fd:已连接 socket,专门用来和客户端通信,收发数据
1.2 业务层简单示例
// 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定IP和端口
int ret = bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// 打开监听
ret = listen(sockfd, 128);
struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr);
// 等待客户端连接
int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cli_len);
// 使用 connfd 与客户端通信
二、accept调用过程
2.1 系统调用入口
关键行为:
- 根据文件描述符查找监听套接字
- 分配一个新的套接字(newsock)用于表示客户端的连接
- 新套接字继承监听套接字的类型(如SOCK_STREAM)和操作函数集
- 分配一个空闲的文件描述符(newfd),将其与新创建的套接字关联起来
- 执行协议层的 accept 操作
- 如果提供了地址缓冲区,获取客户端地址
源码(节选):
// net/socket.c
SYSCALL_DEFINE4(accept4, int, fd, ......)
{
struct socket *sock, *newsock;
struct sockaddr_storage address;
// 根据文件描述符查找监听套接字
sock = sockfd_lookup_light(fd, &err, &fput_needed);
// 分配一个新的套接字,表示客户端的连接
newsock = sock_alloc();
// 新套接字继承监听套接字的类型(如SOCK_STREAM)和操作函数集
newsock->type = sock->type;
newsock->ops = sock->ops;
// 分配一个空闲的文件描述符,将其与新创建的套接字关联起来
newfd = get_unused_fd_flags(flags);
newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
// 实际调用 inet_accept
sock->ops->accept(sock, newsock, sock->file->f_flags);
// 获取客户端地址
if (upeer_sockaddr)
{
newsock->ops->getname(newsock, (struct sockaddr *)&address,&len, 2);
move_addr_to_user(&address, len, upeer_sockaddr, upeer_addrlen);
}
fd_install(newfd, newfile);
}
2.2 inet_accept
关键行为:
- 从全连接队列中取出一个已完成三次握手的子套接字
- 把获取到的子套接字关联到到前面新分配的用户态套接字上
- 将新分配的用户态套接字状态设置为已连接状态
源码(节选):
// net/ipv4/af_inet.c
int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
// sock: 监听 socket sk1: 监听 sock
struct sock *sk1 = sock->sk;
// 实际调用 inet_csk_accept ,从全连接队列中取出一个已完成三次握手的子套接字
struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);
// 把获取到的子套接字关联到前面新分配的用户态套接字上
sock_graft(sk2, newsock);
// 将新分配的用户态套接字状态设置为 SS_CONNECTED(已连接)
newsock->state = SS_CONNECTED;
}
2.3 inet_csk_accept
关键行为:
- 状态判断:如果监听套接字不处于监听状态,直接退出
- 如果连接队列为空,计算等待超时时间:非阻塞返回0;阻塞返回 MAX_SCHEDULE_TIMEOUT,无限等待
- 非阻塞:直接返回 -EAGAIN
- 阻塞:调用 inet_csk_wait_for_connect 让进程休眠,直到有连接到达或超时
- 队列不为空,从队列头部取出一个 request_sock并返回,这表示一个已完成握手的连接请求
源码(节选):
// net/ipv4/inet_connection_sock.c
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
// sk:监听 sock
struct inet_connection_sock *icsk = inet_csk(sk);
// 全连接队列
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
// 如果监听套接字不处于监听状态,直接退出
if (sk->sk_state != TCP_LISTEN)
goto out_err;
// 判断连接队列为空
if (reqsk_queue_empty(queue)) {
// 计算等待超时时间
// 1. 非阻塞返回0
// 2. 阻塞返回 MAX_SCHEDULE_TIMEOUT(无限等待)
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
error = -EAGAIN;
// 非阻塞,直接返回 -EAGAIN
if (!timeo)
goto out_err;
// 阻塞等待,进程睡眠,直到有连接到达或超时
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
// 从队列头部取出一个 request_sock,这表示一个已完成握手的连接请求
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
......
return newsk;
......
}
三、 常见错误码与排查命令
3.1 常见错误码
| 错误码 | 含义 | 常见原因 |
|---|---|---|
EAGAIN / EWOULDBLOCK | 非阻塞 socket,全连接队列为空 | 非阻塞模式下没有就绪连接 |
EINTR | 系统调用被信号中断 | 阻塞等待期间收到信号 |
EINVAL | socket 不支持 accept | sockfd 没有执行 listen或者不是 SOCK_STREAM |
EMFILE | 进程文件描述符达到上限 | fd 耗尽,无法分配新 fd |
ENFILE | 系统文件描述符达到上限 | 系统级打开文件数超限 |
ECONNABORTED | 连接已被中止 | 握手完成后连接被 RST,部分内核版本会返回此错误 |
3.2 排查命令
# 查看进程 fd 使用数
ls /proc/<pid>/fd | wc -l
# 查看进程 fd 上限
cat /proc/<pid>/limits | grep "open files"
# 查看系统级打开文件数
cat /proc/sys/fs/file-nr
查看当前队列情况:
ss -antp
| 状态 | Recv-Q | Send-Q |
|---|---|---|
LISTEN | 全连接队列当前数量 | 全连接队列的最大容量 |
ESTAB | 接收缓冲区中未读取的数据字节数 | 发送缓冲区中未发送的数据字节数 |
四、总结
- accept 不参与三次握手,只是消费全连接队列中内核已经握手完成的连接;
- 返回全新的已连接套接字 fd,监听 socket 本身不受影响,继续接收新连接;
- 阻塞模式队列为空会睡眠等待;非阻塞模式直接返回 EAGAIN。
691



被折叠的 条评论
为什么被折叠?



