本文主要分析socket编程中listen函数的backlog函数进行分析,比较了unix和linux的不同实现,加深对socket编程的理解。主要内容如下:
- listen函数解析
- unix环境参数说明
- linux环境参数说明
- listen函数内核实现
- 队列饱和以后的处理
- listen socket VS accept socket
listen函数解析
listen函数主要用在TCP服务端,将之前通过socket函数创建的sock转换为监听sock,执行listen后的sock对应的TCP状态为LISTEN。在服务端编码中,listen函数执行位置为:
socket->bind->listen->accept。
listen函数格式:
int listen(int sockfd, int backlog);
第一个参数为socket返回的sock描述符,对于的SOCK类型为SOCK_STREAM 或SOCK_SEQPACKET,都属于TCP连接类型。
第二个参数规定了内核为相应套接字排队的最大连接个数。
为了理解第二个参数,必须理解内核是如何管理TCP的连接队列的,这个实现在unix和linux有所不同。
unix环境参数说明
根据《UNIX网络编程卷一》中的描述,unix在管理TCP的被动连接队列时,使用了两条队列,一条是已经接收SYN报文,进入SYN_RCVD状态的连接,服务器正在进行三次握手,这个队列称为未完成连接队列(incomplete connection queue);另一条是连接已经建立,但是尚未被应用accept的队列,这个队列称为已完成连接队列(completed connection queue).如下图所示:
通过图中描述,可以看到backlog为两条队列之和,也就是说,在BSD的实现上,backlog被实现为这两个队列的总和的最大值。
linux环境参数说明
在linux环境上,backlog参数有所不同,使用命令行执行man listen查询函数说明,可以看到对backlog的描述内容为:
The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for com‐pletely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximumlength of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information.
If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128.
从第一段描述看出,Linux内核在2.2以后发生了变化,使用了两个参数来描述两个队列的容量,/proc/sys/net/ipv4/tcp_max_syn_backlog中对应的内核参数来控制未完成连接队列的上限,内核提供了通过listen来控制已完成连接队列的最大值,这个值就是socket函数中的backlog,由此可见,linux实现了被动TCP队列的改进,用两条独立的队列来分别管理未完成队列和已完成队列。
从第二段文字的描述可以看到,backlog设置的最大值并不能随意设置,当用户设置的值大于系统中的somaxconn值时,系统会将对应socket的backlog设置为somaxconn的值。在我测试的系统中,这两个值都是128。
mirusong@ubuntu:~$ uname -r
4.8.0-36-generic
mirusong@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_max_syn_backlog
128
mirusong@ubuntu:~$ cat /proc/sys/net/core/somaxconn
128
listen函数linux内核实现
listen的源代码入口位于socket.c,代码如下:
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
err = security_socket_listen(sock, backlog);
if (!err)
err = sock->ops->listen(sock, backlog);
fput_light(sock->file, fput_needed);
}
return err;
}
上述实现可以看到,linux内核在处理backlog时,获取了系统中的参数somaxconn进行比较,如果大于somaxconn,就会设置backlog为默认的somaxconn,而忽略掉用户设置的值。
AF_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);
err = -EINVAL;
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
old_state = sk->sk_state;
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
/* Really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
*/
if (old_state != TCP_LISTEN) {
/* Check special setups for testing purpose to enable TFO w/o
* requiring TCP_FASTOPEN sockopt.
* Note that only TCP sockets (SOCK_STREAM) will reach here.
* Also fastopenq may already been allocated because this
* socket was in TCP_LISTEN state previously but was
* shutdown() (rather than close()).
*/
if ((sysctl_tcp_fastopen & TFO_SERVER_ENABLE) != 0 &&
inet_csk(sk)->icsk_accept_queue.fastopenq == NULL) {
if ((sysctl_tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) != 0)
err = fastopen_init_queue(sk, backlog);
else if ((sysctl_tcp_fastopen &
TFO_SERVER_WO_SOCKOPT2) != 0)
err = fastopen_init_queue(sk,
((uint)sysctl_tcp_fastopen) >> 16);
else
err = 0;
if (err)
goto out;
}
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;
}
sk->sk_max_ack_backlog = backlog;
err = 0;
out:
release_sock(sk);
return err;
}
这个函数主要完成listen中的状态迁移,同时根据结果设置backlog的值,这样通过设置对于sock结构上的值来实现队列的管理。
linux队列饱和以后的处理
上面分析到的两条队列,在系统运行时可能存在队列饱和的情况,这个时候,如何处理新到的请求将会决定系统的对外行为。
当已连接队列饱和的时候,如果未完成连接队列中的socket接收到了客户端发来的最后一个ACK,这个时候,由于服务端队列已满,不能处理最近到达的请求,这个时候,ACK请求会被丢弃处理,具体如何回复客户端,取决于tcp_abort_on_overflow中的设置,如果为0,则直接丢弃该ACK,如果为1,则回复RST报文给客户端,开启这个开关需要谨慎,这种情况下,客户端的应用程序会对这种情况做处理,看是否需要进行特殊处理,因为客户端需要区分到底是没有开启服务还是已连接队列已经饱和。ip-sysctl.txt对tcp_abort_on_overflow的描述如下:
If listening service is too slow to accept new connections, reset them. Default state is FALSE. It means that if overflow occurred due to a burst, connection will recover. Enable this option only if you are really sure that listening daemon cannot be tuned to accept connections faster. Enabling this option can harm clients of your server.
当未完成连接队列满的时候,新到的SYN请求报文会被丢弃,这种情况多见于SYN flood攻击,这个时候把syn队列加大,可以缓解此类攻击,修改参数为tcp_max_syn_backlog,这个参数能否生效依赖于参数tcp_syncookies,tcp_syncookies开启 的时候,tcp_max_syn_backlog自动失效,这个参数被忽略。
listen socket VS accept socket
在socket编程中,需要区分监听socket和已经连接成功的socket,服务器上的监听socket一般情况下不会指定对端的地址和端口信息,只指定了本地的端口号,这种一般以服务的形态对外体现,比如常见的ssh,http,http等,三次握手成功以后,内核在完成最后一次握手的时候,会重新申请新的socket,这个socket是由五元组唯一标识的socket,这类socket是应用通过accept后从监听socket中独立出来的,后续报文收发均在这个socket上处理,但是监听socket只绑定了本地端口号,通过本地端口来标识,这点是他们之间的不同之处。需要注意这种差别。