CLOVER🍀

That was when it all began.

ssコマンドのRecv-Qが、backlog+1の値まで上がるのはどうして?

これは、なにをしたくて書いたもの?

以前、Linuxのbacklogについてちょっと調べてみました。

Linuxのbacklogについて調べてみる - CLOVER🍀

この時、ssコマンドで見ると「Send-Q」の値がbacklogの値となるのですが、「Recv-Q」の値はbacklog+1まで上がるので、「一緒のところで
頭打ちにならないんだ?」ということがちょっと気になり、もう少し見てみることにしました。

環境

今回の環境は、こちらです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.2 LTS
Release:    18.04
Codename:   bionic


$ uname -a
Linux xxxxx 4.15.0-55-generic #60-Ubuntu SMP Tue Jul 2 18:22:20 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

Ubuntu Linux 18.04 LTS、カーネルは4.15です。

また、ssコマンドのバージョンはこちら。

$ ss -v
ss utility, iproute2-ss180129

ssコマンドはiproute2パッケージの一部なので、こちらのバージョンも。

$ dpkg -l | grep iproute2
ii  iproute2                                      4.15.0-2ubuntu1                              amd64        networking and traffic control tools

4.15ですね。

サンプルプログラム

確認に使ったプログラムは、こちらです。
silent_server.py

import socket
import time

from datetime import datetime

host = '0.0.0.0'
port = 8080
bind_address = (host, port)

backlog_size = 2 

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(bind_address)
    server_socket.listen(backlog_size)

    print('[{}] Server startup'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))

    try:
        client_socket, addr = server_socket.accept()

        remote_addr = client_socket.getpeername()
    
        print('[{}] - handle connection, start - {}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), remote_addr))
    
        with client_socket:
            while True:
                time.sleep(1)

    except KeyboardInterrupt:
        print('[{}] Server stop'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))

TCPソケットをひとつ受け付けたら、そのままなにもしないプログラムです。他の接続は、そのまま積み上がっていきます。

Pythonのバージョンは、こちら。

$ python3 -V
Python 3.6.8

ssコマンドはなにを表示している?

とりあえず、プログラムを起動して

$ python3 silent_server.py 
[2019-07-27 19:21:54] Server startup

telnetでいくつかアクセスしてみます。

$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.


$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.


$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.


$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

この時のssコマンドの結果。

$ ss -tnl
State                   Recv-Q                   Send-Q                                        Local Address:Port                                       Peer Address:Port                   
LISTEN                  3                        2                                                   0.0.0.0:8080                                            0.0.0.0:*

backlogの値を超えたので、これ以上は接続待ちになります。

で、ssコマンドがなにを表示しているのかという話なのですが、ちょっとソースコードを取得してみましょう。

Index of /pub/linux/utils/net/iproute2/

$ wget https://mirrors.edge.kernel.org/pub/linux/utils/net/iproute2/iproute2-4.15.0.tar.xz
$ tar xf iproute2-4.15.0.tar.xz
$ cd iproute2-4.15.0

ssコマンドは、「misc/ss.c」というファイルに実装されています。

Recv-Qの内容は、sockstat構造体のrqの値を出力しています。

 field_set(COL_RECVQ);
    out("%-6d", s->rq);
struct sockstat {
    struct sockstat       *next;
    unsigned int      type;
    uint16_t       prot;
    uint16_t       raw_prot;
    inet_prefix     local;
    inet_prefix     remote;
    int            lport;
    int            rport;
    int            state;
    int            rq, wq;
    unsigned int ino;
    unsigned int uid;
    int            refcnt;
    unsigned int      iface;
    unsigned long long  sk;
    char *name;
    char *peer_name;
    __u32           mark;
};

sockstat#rqの値は、inet_diag_msg#idiag_rqueueの値がコピーされたものです。

static void parse_diag_msg(struct nlmsghdr *nlh, struct sockstat *s)
{
    struct rtattr *tb[INET_DIAG_MAX+1];
    struct inet_diag_msg *r = NLMSG_DATA(nlh);

    parse_rtattr(tb, INET_DIAG_MAX, (struct rtattr *)(r+1),
             nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*r)));

    s->state = r->idiag_state;
    s->local.family  = s->remote.family = r->idiag_family;
    s->lport = ntohs(r->id.idiag_sport);
    s->rport = ntohs(r->id.idiag_dport);
    s->wq        = r->idiag_wqueue;
    s->rq        = r->idiag_rqueue;
    s->ino       = r->idiag_inode;
    s->uid       = r->idiag_uid;
    s->iface = r->id.idiag_if;
    s->sk        = cookie_sk_get(&r->id.idiag_cookie[0]);

sockstat#wqは、inet_diag_msg#idiag_wqueueの値がコピーされたものですね。

inet_diag_msg構造体の定義。

https://github.com/torvalds/linux/blob/v4.15/include/uapi/linux/inet_diag.h#L109-L124

manで、idiag_rqueueの説明を見てみます。ついでに、idiag_wqueueも。

sock_diag(7) - Linux manual page

   idiag_rqueue  
          For listening sockets: the number of pending connections.  

          For other sockets: the amount of data in the incoming queue.  

   idiag_wqueue  
          For listening sockets: the backlog length.  

          For other sockets: the amount of memory available for sending.

idiag_wqueueがbacklogの長さ、idiag_rqueueがaccept待ちの接続数ですね。

この2つの値は、sock構造体のsk_ack_backlog、sk_max_ack_backlogがそれぞれコピーされたものです。

https://github.com/torvalds/linux/blob/v4.15/net/ipv4/tcp_diag.c#L28-L29

static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r,
                  void *_info)
{
    struct tcp_info *info = _info;

    if (sk_state_load(sk) == TCP_LISTEN) {
        r->idiag_rqueue = sk->sk_ack_backlog;
        r->idiag_wqueue = sk->sk_max_ack_backlog;

ちなみにsock構造体のこの2つのメンバー、backlogがいっぱいかどうかの判定に使われていたものです。

https://github.com/torvalds/linux/blob/v4.15/include/net/sock.h#L833-L836

static inline bool sk_acceptq_is_full(const struct sock *sk)
{
    return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

sk_ack_backlogの値を上げたり下げたりしているのは、このあたりです。

linux/sock.h at v4.15 · torvalds/linux · GitHub

というか、よくよく考えると判定条件が「accept待ちの接続数 > backlog」になっている時点で、+1付きますね…。

static inline bool sk_acceptq_is_full(const struct sock *sk)
{
    return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

前回見た時、あんまり考えてなかったです…。

ということは、backlogの値を0にしても、最低ひとつはaccept待ちのソケットを持てるということですね。

試してみましょう。

Pythonプログラムのbacklogの値を、0に修正します。

import socket
import time

from datetime import datetime

host = '0.0.0.0'
port = 8080
bind_address = (host, port)

backlog_size = 0

これでプログラムを起動し、telnetでアクセスするとこんな感じに。

$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.


$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.


$ telnet localhost 8080
Trying 127.0.0.1...

3つ目は、接続できていませんね。

この時のssコマンドの結果。

$ ss -tnl
State                   Recv-Q                   Send-Q                                        Local Address:Port                                       Peer Address:Port                   
LISTEN                  1                        0                                                   0.0.0.0:8080                                            0.0.0.0:*

なるほど。

まとめ

accept待ちのソケットの数が、backlog+1になる件をssコマンドのソースコードから、Linuxのソースコードまでを追って見てみました。

backlog+1までaccept待ちの接続にできる、覚えておきましょう。

参考)

How TCP backlog works in Linux

オマケ

そういえば、backlogのサイズはいくつまで?とふと思ったので見てみたのですが、符号なし32ビット整数の範囲みたいですね。

https://github.com/torvalds/linux/blob/v4.15/include/net/sock.h#L458-L459

 u32         sk_ack_backlog;
    u32         sk_max_ack_backlog;

以前は符号なし16ビットだったようなので、カーネルのバージョンとともに変わっていくものなのでしょうね。