目录
前言
socket()创建套接字、bind()绑定 IP 与端口。完成 bind 之后,socket 还只是处于CLOSED状态,并不能接收客户端连接,想要对外提供服务,必须调用listen()。很多人都知道listen开启监听,但对于下面的问题是否清楚:
- listen在内核中完成了哪些工作?
- backlog参数限制的哪一个队列?
- 半连接、全连接队列什么区别?
一、系统调用入口
1.1 函数原型
int ret = listen(fd, backlog);
参数详解:
- fd:socket()调用返回的文件描述符
- backlog:全连接队列的最大长度
返回值: 成功返回 0;失败返回‑1
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);
二、关键数据结构
// include/net/request_sock.h
struct request_sock_queue
{
// 当前半连接数
atomic_t qlen;
atomic_t young;
// 全连接队列指针
struct request_sock *rskq_accept_head;
struct request_sock *rskq_accept_tail;
};
- 全连接队列:完成三次握手,状态切换为 ESTABLISHED 并且没有被accept()取走的的socket。backlog参数限制的是全连接队列的最大长度,同时全连接队列上限不能超过系统全局参数net.core.somaxconn,最终队列大小取「backlog 和 somaxconn 的较小值」
- 半连接队列:收到 SYN 后、第三次握手 ACK 到来前,服务端创建一个struct request_sock记录对方信息存入该队列,并发送 SYN+ACK,等待客户端最后一次 ACK
- 结构体字段释义:
qlen:所有半连接的数量young:半连接中尚未经过第一次 SYN+ACK 重传定时器处理的数量rskq_accept_head/rskq_accept_tail:全连接队列指针
- 二者行为表现后续三次握手详细讲解
划重点:TCP三次握手是内核协议栈自动完成,
listen()只负责切换监听状态、初始化队列;accept()并不参与握手,仅仅是把已经握手完成的连接从全连接队列取出交给应用程序。
三、listen调用过程
3.1 系统调用入口
关键行为:
- 根据文件描述符 fd 查找到内核socket对象;
- 做参数校验,如果应用程序传入的 backlog 大于somaxconn,直接截断为somaxconn;
- 调用协议层真正的listen实现,即inet_listen。
源码(节选):
// net/socket.c
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
struct socket *sock;
int somaxconn;
// 由 fd 获取 监听的 socket
sock = sockfd_lookup_light(fd, &err, &fput_needed);
// backlog不能超过core.sysctl_somaxconn
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
// 实际调用 inet_stream_ops 中的 inet_listen
sock->ops->listen(sock, backlog);
}
3.2 inet_listen
关键行为:
- socket 层前置检查:只允许流式 TCP 套接字,套接字必须处于未连接状态;
- 如果 socket 还没有进入 LISTEN 状态,调用
inet_csk_listen_start正式开启监听; - 如果 socket 已经是 LISTEN 状态,仅更新全连接队列上限。
源码(节选):
// net/ipv4/af_inet.c
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
// socket 必须是未连接状态(排除已connect()的客户端socket、已accept()的socket 等)
// 只对 SOCK_STREAM 流式 TCP 套接字生效
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
old_state = sk->sk_state;
// sock 的状态必须是 TCP_CLOSE 和 TCP_LISTEN
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
if (old_state != TCP_LISTEN)
// 真正将 socket 变为监听状态的核心函数
inet_csk_listen_start(sk, backlog);
// 不管新 listen 还是已 listen,都会走到这里
sk->sk_max_ack_backlog = backlog;
out:
return err;
}
3.3 inet_csk_listen_start
关键行为:
- 调用reqsk_queue_alloc初始化连接队列`icsk_accept_queue;
- 设置全连接队列上限
sk_max_ack_backlog;sk_ack_backlog为当前全连接队列长度,初始为 0; - 将 sock 内核状态修改为
TCP_LISTEN,开启监听; - 如果调用 bind 绑定过端口,这里就是那个端口;否则内核自动分配一个空闲端口,把监听 sock 加入监听哈希表。
源码(节选):
// net/ipv4/inet_connection_sock.c
int inet_csk_listen_start(struct sock *sk, int backlog)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct inet_sock *inet = inet_sk(sk);
// 初始化全连接队列
reqsk_queue_alloc(&icsk->icsk_accept_queue);
// 全连接队列长度上限
sk->sk_max_ack_backlog = backlog;
// 当前全连接队列的长度,初始为0
sk->sk_ack_backlog = 0;
// 将sock状态改为TCP_LISTEN
sk_state_store(sk, TCP_LISTEN);
// 如果调用 bind 绑定过端口,这里就是那个端口;否则内核自动分配一个空闲端口
if (!sk->sk_prot->get_port(sk, inet->inet_num))
{
inet->inet_sport = htons(inet->inet_num);
// 将监听 sock 加入 listening_hash
sk->sk_prot->hash(sk);
}
}
3.3 reqsk_queue_alloc
关键行为:初始化全连接队列链表
源码(节选):
// net/core/request_sock.c
void reqsk_queue_alloc(struct request_sock_queue *queue)
{
queue->rskq_accept_head = NULL;
}
四、相关内核参数查看命令
# 查看系统somaxconn配置
cat /proc/sys/net/core/somaxconn
# 查看半连接队列配置
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
# 查看syn‑cookie开关状态
cat /proc/sys/net/ipv4/tcp_syncookies
五、总结
listen()方法主要做了以下几件事情:
- 将 socket 从连接创建状态(
TCP_CLOSE)切换为监听状态(TCP_LISTEN),使其能够接收客户端连接请求,初始化全连接队列,TCP三次握手过程由内核自动完成。 - backlog 控制全连接队列上限,最终生效值会被
somaxconn系统参数截断 - 如果 socket 已经是监听状态,则只允许调整 backlog 大小
- TCP 三次握手由内核自动完成;握手完成的连接放到全连接队列排队,等待
accept()取出


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



