hana_shinのLinux技術ブログ

Linuxの技術情報を掲載しています。特にネットワークをメインに掲載していきます。

ソケットオプションの使い方(SO_LINGER編)



1 SO_LINGERとは?

1.1 概要

SO_LINGERは、closeシステムコールから復帰するタイミングを制御するソケットオプションです。このオプションを使うと、FINに対するACKを受信したあとcloseシステムコールから復帰できるようになります。

具体例を以下に説明します。
SO_LINGERを使わない場合、closeシステムコールを実行すると、すぐにcloseシステムコールから復帰します。

      client                                              server
        |                                                   |
        |                                                   |
close() |----------------------- FIN ---------------------->|
復帰 <--|                                                   |
        |                                                   |
        |<---------------------- FIN + ACK -----------------|
        |                                                   |
        |                                                   |
        |----------------------- ACK ---------------------->|
        |                                                   |

一方、SO_LINGERを使うと、FINに対するACKを受信してから、closeシステムコールが復帰します。つまり、SO_LINGERを使うと、クライアントが送信したFINがサーバに届いたことがわかります。

      client                                              server
        |                                                   |
        |                                                   |
close() |----------------------- FIN ---------------------->|
        |                                                   |
        |                                                   |
復帰<-- |<---------------------- FIN + ACK -----------------|
        |                                                   |
        |                                                   |
        |----------------------- ACK ---------------------->|
        |                                                   |

1.2 補足

closeシステムコールを実行すると、FINパケットが送信されます。通常、クライアント、サーバでそれぞれcloseシステムコールを実行してTCPコネクションを開放します。サーバがFINを受信してcloseシステムコールを実行するまでの時間が長いと、以下のようにACKとFINが別々のTCPパケットでクライントに送信されます。

      client                                              server
        |                                                   |
        |                                                   |
close() |----------------------- FIN ---------------------->|
        |                                                   |
        |<---------------------- ACK -----------------------|
        |                                                   |
        |<---------------------- FIN -----------------------| close()
        |                                                   |
        |----------------------- ACK ---------------------->|
        |                                                   |

しかし、サーバがFINを受信してcloseシステムコールを実行するまでの時間が短い場合、以下のようにACKとFINが1つのTCPパケットにまとめられてクライアントに送信されます。

      client                                              server
        |                                                   |
        |                                                   |
close() |----------------------- FIN ---------------------->|
        |                                                   |
        |<---------------------- FIN + ACK -----------------| close()
        |                                                   |
        |----------------------- ACK ---------------------->|
        |                                                   |

2 検証環境

2.1 ネットワーク構成

サーバとクライアントの2台構成です。図中のeth0はNICの名前です。

                            192.168.2.0/24
client(eth0) -------------------------------------------- (eth0) server
            .105                                     .100

2.2 版数

サーバ、クライアントともに下記版数です。

[root@server ~]# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)

3 テストプログラム(TP)作成

テストプログラムを作成して、SO_LINGERの使用有無によって、closeの復帰タイミングがどのように変わるかを確認してみます。なお、エラー処理は見やすくするため意図的に省略しています。また、ソケットオプションの動作確認を目的としているため、実用的なプログラムにはなっていません。

3.1 サーバ側

サーバで動作するTPを作成します。

[root@server ~]# vi cl.c
[root@server ~]# cat sv.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
    int lfd, cfd;
    socklen_t len;
    struct sockaddr_in sv, cl;
    char buf[32];
    ssize_t n;

    lfd = socket(AF_INET, SOCK_STREAM, 0);

    sv.sin_family = AF_INET;
    sv.sin_port = htons(11111);
    sv.sin_addr.s_addr = INADDR_ANY;

    bind(lfd, (struct sockaddr *)&sv, sizeof(sv));
    listen(lfd, 5);
    memset(buf, 0, sizeof(buf));
    len = sizeof(cl);
    cfd = accept(lfd, (struct sockaddr *)&cl, &len);

    while (1) {
        n = read(cfd, buf, sizeof(buf));
        if(n > 0)
            fprintf(stderr,"%zd, %s\n", n, buf);
        else if(n == 0) {  //EOF
            close(cfd);
            return 0;
        }
        else {
            perror("read");
            return 1;
        }
    }
    close(lfd);
    return 0;
}

3.2 クライアント側

クライアントで動作するTPを作成します。
setsockoptシステムコールを使ってソケットにSO_LINGERを設定します。以下の例では、SO_LINGERのタイムアウト時間を5秒に設定しています。タイムアウトは、FINに対するACKがかえってこなかった場合、closeシステムコールから復帰する時間です。また、sleep(10)は、スリープしている間にiptablesコマンドを実行するための時間です。

[root@client ~]# vi cl.c
[root@client ~]# cat cl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in server;
    struct linger lin;

    sock = socket(AF_INET, SOCK_STREAM, 0);

    server.sin_family = AF_INET;
    server.sin_port = htons(11111);
    server.sin_addr.s_addr = inet_addr("192.168.2.100");

    if(argc != 2) {
        printf("Usage: ./cl on|off\n");
        exit(1);
    }
    if(!strncmp("on", argv[1], 2)) {
      printf("SO_LINGER is enabled\n");
      lin.l_onoff = 1;
      lin.l_linger = 5;
    }
    else if(!strncmp("off", argv[1], 3)) {
      printf("SO_LINGER is disabled\n");
      lin.l_onoff = 0;
    }
    else {
        printf("Usage: ./cl on|off\n");
        exit(1);
    }

    setsockopt(sock, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
    connect(sock, (struct sockaddr *)&server, sizeof(server));

    write(sock, "123", sizeof("123"));
    printf("I'm going to sleep for 10 seconds\n");
    sleep(10);
    close(sock);
    return 0;
}

3.3 コンパイル

サーバのテストプログラムをコンパイルします。

[root@server ~]# gcc -Wall -o sv sv.c
[root@server ~]# ls -l sv*
-rwxr-xr-x. 1 root root 8904  1月  9 09:17 sv
-rw-r--r--. 1 root root  860  1月  9 09:17 sv.c

クライアントのテストプログラムをコンパイルします。

[root@client ~]# gcc -Wall -o cl cl.c
[root@client ~]# ls -l cl*
-rwxr-xr-x. 1 root root    8768  1月  9 09:19 cl
-rw-r--r--. 1 root root     642  1月  9 09:15 cl.c

4 事前準備

サーバのテストプログラムは、TCPの11111番ポートでListenするので、TCPの11111番ポートを開放します。firewall-cmdの使い方は、firewall-cmdの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# firewall-cmd --add-port=11111/tcp
success

ポートの状態を確認します。TCPの11111番ポートが開放されたことがわかります。

[root@server ~]# firewall-cmd --list-ports
11111/tcp

5 動作確認

5.1 SO_LINGERを使わない場合

サーバのテストプログラムを実行します。

[root@server ~]# ./sv

tcpdumpコマンドを実行します。"-ttt "は採取したパケットとパケットの時刻差分を表示するオプションです。tcpdumpコマンドの使い方は、tcpdumpの使い方(基本編) - hana_shinのLinux技術ブログを参照してください。

[root@client ~]# tcpdump -ttt -i eth0 port 11111 -nn

10秒経過するとcloseシステムコールが実行されます。このとき、closeシステムコールの実行時間が371マイクロ秒(★)であることがわかります。straceコマンドの使い方は、straceコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@client ~]# strace -T -e trace=close ./cl off
close(3)                                = 0 <0.000067>
close(3)                                = 0 <0.000036>
SO_LINGER is disabled
I'm going to sleep for 10 seconds
close(3)                                = 0 <0.000371> ★
+++ exited with 0 +++

一方、FINに対するACK(2つ目のパケット)は、3379マイクロ秒後に受信していることがわかります。

[root@client ~]# tcpdump -ttt -i eth0 port 11111 -nn
-snip-
 00:00:10.012256 IP 192.168.2.105.42180 > 192.168.2.100.11111: Flags [F.], seq 5, ack 1, win 229, options [nop,nop,TS val 6796667 ecr 6908639], length 0
 00:00:00.003379 IP 192.168.2.100.11111 > 192.168.2.105.42180: Flags [F.], seq 1, ack 6, win 227, options [nop,nop,TS val 6918652 ecr 6796667], length 0
 00:00:00.000114 IP 192.168.2.105.42180 > 192.168.2.100.11111: Flags [.], ack 2, win 229, options [nop,nop,TS val 6796669 ecr 6918652], length 0

上記結果をまとめると、以下のようになります。closeシステムコールは、FINに対するACKを待たずに、復帰していることがわかります。

      client                                              server
        |                                                   |
        |                                                   |
close() |----------------------- FIN ---------------------->|
        |   *                *                              |
        |   |                |                              |
        |  371 micorseconds  |                              |
        |   |                |                              |
復帰 <--|   *              3379 micorseconds                |
        |                    |                              |
        |                    |                              |
        |                    |                              |
        |                    *                              |
        |<---------------------- FIN + ACK -----------------|
        |                    *                              |
        |                    |                              |
        |                  114 micorseconds                 |
        |                    |                              |
        |                    *                              |
        |----------------------- ACK ---------------------->|
        |                                                   |

5.2 SO_LINGERを使った場合

5.2.1 タイムアウト時間内にFINに対するACKを受信できない場合

サーバでテストプログラムを実行します。

[root@server ~]# ./sv

クライアントでテストプログラムを実行します。

[root@client ~]# strace -T -e trace=close ./cl on
close(3)                                = 0 <0.000049>
close(3)                                = 0 <0.000015>
SO_LINGER is enabled
I'm going to sleep for 10 seconds

テストプログラムがスリープしている間にiptablesを実行して、FINに対するACKを破棄する設定をします。FINに対するACKを受信できないため、SO_LINGERオプションのタイムアウトが発生します。

[root@client ~]# iptables -I INPUT -p tcp --sport 11111 -j DROP

設定を確認します。

[root@client ~]# iptables -nvL INPUT --line-numbers
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DROP       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp spt:11111

10秒経過するとcloseシステムコールが実行されます。このとき、closeシステムコールの実行時間が約5秒(★)であることがわかります。

[root@client ~]# strace -T -e trace=close ./cl on
close(3)                                = 0 <0.000049>
close(3)                                = 0 <0.000015>
SO_LINGER is enabled
I'm going to sleep for 10 seconds
close(3)                                = 0 <5.015066> ★
+++ exited with 0 +++

なお、iptablesの設定を削除するには、以下のように実行します。削除するルールは”-line-numbers”で確認した1番を指定します。

[root@client ~]# iptables -D INPUT 1

上記をまとめると、以下のようになります。closeシステムコールは、SO_LINGERのタイムアウトにより復帰していることがわかります。

      client                                              server
        |                                                   |
        |                                                   |
close() |----*------------------ FIN ---------------------->|
discard |<---|------------------ FIN + ACK -----------------|
        |    |                                              |
        |----|------------------ FIN(retrans) ------------->|
discard |<---|------------------ FIN + ACK -----------------|
        |    |                                              |
        |----|------------------ FIN(retrans) ------------->|
discard |<---|------------------ FIN + ACK -----------------|
        |    |                                              |
        |    |                                              |
        |  5.01 seconds                                     |
        |    |                                              |
        |    |                                              |
復帰 <--|    *                                              |
        |                                                   |
5.2.2 タイムアウト時間内にFINに対するACKを受信できた場合

サーバでtcコマンドを実行して、FINに対するACK送信を100ミリ秒遅延させてみます。クライアントのcloseシステムコールの実行時間が100ミリ秒になることを確認してみます。

サーバでテストプログラムを実行します。

[root@server ~]# ./sv

クライアントでテストプログラムを実行します。

[root@client ~]# strace -T -e trace=close ./cl on
close(3)                                = 0 <0.000446>
close(3)                                = 0 <0.000317>
SO_LINGER is enabled
I'm going to sleep for 10 seconds

テストプログラムがスリープしている間に、サーバでtcコマンドを実行して、FINに対するACK送信を100ミリ秒遅延させてみます。

[root@server ~]# tc qdisc add dev eth0 root netem delay 100ms

設定を確認します。

[root@server ~]# tc qdisc show dev eth0
qdisc netem 8006: root refcnt 2 limit 1000 delay 100.0ms

10秒経過するとcloseシステムコールが実行されます。このとき、closeシステムコールの実行時間が約108ミリ秒(★)であることがわかります。サーバでACK送信を遅延させた分、closeシステムコールの復帰が遅れていることがわかります。

[root@client ~]# strace -T -e trace=close ./cl on
close(3)                                = 0 <0.000446>
close(3)                                = 0 <0.000317>
SO_LINGER is enabled
I'm going to sleep for 10 seconds
close(3)                                = 0 <0.108881> ★
+++ exited with 0 +++

なお、tcコマンドの設定を削除するには、以下のように実行します。

[root@server ~]# tc qdisc del dev eth0 root

上記をまとめると、以下のようになります。closeシステムコールは、FINに対するACKを待って(約108ミリ秒)から復帰していることがわかります。

      client                                              server
        |                                                   |
        |                                                   |
close() |----------------------- FIN ---------------------->|
        |                    *                              |
        |                    |                              |
        |                    |                              |
        |                    |                              |
        |                 108 millisecond                   |
        |                    |                              |
        |                    |                              |
        |                    |                              |
        |                    *                              |
復帰 <--|<---------------------- FIN + ACK -----------------|
        |                                                   |
        |----------------------- ACK ---------------------->|
        |                                                   |

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ