深入Linux网络子系统03:调用 listen 之后,内核发生了哪些事?


前言

socket()创建套接字、bind()绑定 IP 与端口。完成 bind 之后,socket 还只是处于CLOSED状态,并不能接收客户端连接,想要对外提供服务,必须调用listen()。很多人都知道listen开启监听,但对于下面的问题是否清楚:

  1. listen在内核中完成了哪些工作?
  2. backlog参数限制的哪一个队列?
  3. 半连接、全连接队列什么区别?

一、系统调用入口

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;
};
  1. 全连接队列:完成三次握手,状态切换为 ESTABLISHED 并且没有被accept()取走的的socket。backlog参数限制的是全连接队列的最大长度,同时全连接队列上限不能超过系统全局参数net.core.somaxconn,最终队列大小取「backlog 和 somaxconn 的较小值」
  2. 半连接队列:收到 SYN 后、第三次握手 ACK 到来前,服务端创建一个struct request_sock记录对方信息存入该队列,并发送 SYN+ACK,等待客户端最后一次 ACK
  3. 结构体字段释义
  • qlen:所有半连接的数量
  • young:半连接中尚未经过第一次 SYN+ACK 重传定时器处理的数量
  • rskq_accept_head/rskq_accept_tail:全连接队列指针
  1. 二者行为表现后续三次握手详细讲解

划重点:TCP三次握手是内核协议栈自动完成,listen()只负责切换监听状态、初始化队列;accept()并不参与握手,仅仅是把已经握手完成的连接从全连接队列取出交给应用程序。

三、listen调用过程

3.1 系统调用入口

关键行为:

  1. 根据文件描述符 fd 查找到内核socket对象;
  2. 做参数校验,如果应用程序传入的 backlog 大于somaxconn,直接截断为somaxconn;
  3. 调用协议层真正的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

关键行为:

  1. socket 层前置检查:只允许流式 TCP 套接字,套接字必须处于未连接状态;
  2. 如果 socket 还没有进入 LISTEN 状态,调用inet_csk_listen_start正式开启监听;
  3. 如果 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

关键行为:

  1. 调用reqsk_queue_alloc初始化连接队列`icsk_accept_queue;
  2. 设置全连接队列上限sk_max_ack_backlogsk_ack_backlog为当前全连接队列长度,初始为 0;
  3. 将 sock 内核状态修改为TCP_LISTEN,开启监听;
  4. 如果调用 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()方法主要做了以下几件事情:

  1. 将 socket 从连接创建状态(TCP_CLOSE)切换为监听状态TCP_LISTEN),使其能够接收客户端连接请求,初始化全连接队列,TCP三次握手过程由内核自动完成。
  2. backlog 控制全连接队列上限,最终生效值会被somaxconn系统参数截断
  3. 如果 socket 已经是监听状态,则只允许调整 backlog 大小
  4. TCP 三次握手由内核自动完成;握手完成的连接放到全连接队列排队,等待accept()取出
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值