これは、なにをしたくて書いたもの?
以前、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ビットだったようなので、カーネルのバージョンとともに変わっていくものなのでしょうね。