编写服务端程序时,需要调用 listen()
系统调用来开始监听请求连接,listen()
系统调用的原型如下:
int listen(int sockfd, int backlog);
可以看到,listen()
系统调用需要传入两个参数,第一个 sockfd
表示监听的 socket 句柄,而 backlog
参数表示接收请求队列的长度。对于第一个参数比较容易理解,那么第二个参数的作用是什么呢?下面我们来分析一下。
listen() 系统调用在内核的实现
当我们在程序中调用 listen()
系统调用时,会触发调用内核的 sys_listen()
函数,sys_listen()
函数的实现如下:
asmlinkage long sys_listen(int fd, int backlog){ struct socket *sock; int err; if ((sock = sockfd_lookup(fd, &err)) != NULL) { /* 步骤1 */ if ((unsigned) backlog > SOMAXCONN) backlog = SOMAXCONN; err=sock->ops->listen(sock, backlog); /* 步骤2 */ sockfd_put(sock); } return err; }
sys_listen()
函数的实现比较简单,过程如下:
- 步骤1:首先调用
sockfd_lookup()
函数查找文件句柄fd
对应的socket
对象。 - 步骤2:通过调用
socket
对象的的listen()
方法来进行监听操作。
对于 TCP协议
来说,socket
对象的 listen()
方法会绑定到 inet_listen()
函数。所以 步骤2
最后会调用 inet_listen()
函数,inet_listen()
函数的实现如下:
int inet_listen(struct socket *sock, int backlog) { struct sock *sk = sock->sk; unsigned char old_state; int err; lock_sock(sk); ... old_state = sk->state; if (!((1<<old\_state)&(TCPF\_CLOSE|TCPF_LISTEN))) goto out; if (old\_state != TCP\_LISTEN) { err = tcp\_listen\_start(sk); /* 步骤1 */ if (err) goto out; } sk->max\_ack\_backlog = backlog; /* 步骤2 */ err = 0; out: release_sock(sk); return err; }
inet_listen()
函数的实现也非常简单,主要分为两个步骤:
- 步骤1:调用
tcp_listen_start()
函数把socket
对象的状态设置为TCP_LISTEN
。 - 步骤2:把
socket
对象的sk
成员变量的max_ack_backlog
字段设置为backlog
。
其中 max_ack_backlog
字段就是用于保存最大接收连接队列的长度,至此 listen()
函数的工作就完成了,那么内核在哪里限制接收连接队列的呢?
内核限制TCP连接队列
当网卡接收到数据时,会接收到数据包并封装成 sk_buff
对象,如果接收到的数据包是一个 TCP协议
的数据包,那么内核将会把数据包提交给 tcp_v4_rcv()
函数处理。我们只关注限制TCP连接队列的实现,所以这里直接给出限制逻辑相关的调用链:
tcp\_v4\_rcv() \\__> tcp\_v4\_do_rcv() \\__> tcp\_v4\_hnd_req() \\__> tcp\_check\_req() \\__> tcp\_v4\_syn\_recv\_sock()
从上面的调用链可以看出,最后调用的函数是 tcp_v4_syn_recv_sock()
,tcp_v4_syn_recv_sock()
函数的作用是当对端连接完成 TCP三次握手
后,将创建一个新的 socket 连接对象。我们来看看 tcp_v4_syn_recv_sock()
函数对连接队列的限制逻辑:
tcp\_v4\_syn\_recv\_sock(struct sock *sk, struct sk_buff *skb, struct open_request *req, struct dst_entry *dst) { struct tcp_opt *newtp; struct sock *newsk; if (tcp\_acceptq\_is_full(sk)) /* 判断接收队列是否超过限制 */ goto exit_overflow; ... return newsk; exit_overflow: NET\_INC\_STATS_BH(ListenOverflows); exit: NET\_INC\_STATS_BH(ListenDrops); dst_release(dst); return NULL; }
tcp_v4_syn_recv_sock()
函数首先调用了 tcp_acceptq_is_full()
来判断接收队列是否已经超过限制,如果超过限制就不再创建新的连接,tcp_acceptq_is_full()
函数的实现如下:
static inline int tcp\_acceptq\_is_full(struct sock *sk) { return sk->ack\_backlog > sk->max\_ack_backlog; }
tcp_acceptq_is_full()
函数很简单,就是判断当前接收队列的数量是否超过了限制的最大数量,如果是就返回true,至此我们对 backlog
参数的作用分析完毕。
原文:https://mp.weixin.qq.com/s/DfJvIoLpaU5G1H-462qjRQ