hana_shinのLinux技術ブログ

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

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



1 SO_SNDTIMEOとは?

SO_SNDTIMEOは、connectシステムコールや送信系(write,send,sendto,sendmsg)システムコールに対して使用するソケットオプションです。これらシステムコールのデフォルト動作はブロッキングモードです。そのため、以下の状況のとき、システムコールは実行完了しません(ブロックしたままになります)。しかし、SO_SNDTIMEOを使用すると、指定した時間でシステムコールの実行を完了することができます。
・connectシステムコール:相手からSYN+ACKパケットを受信できない場合(別途記事作成予定)
・送信系システムコール:ソケットの送信バッファに空きスペースがない場合(今回の記事はこちらが対象)

ここでは、送信系システムコールの動作確認をしてみたいと思います。
なお、SO_RCVTIMEOについては、ソケットオプションの使い方(SO_RCVTIMEO編) - hana_shinのLinux技術ブログを参照してください。

2 検証環境

2.1 ネットワーク構成

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

                               192.168.122.0/24
client(enp1s0) ------------------------------------------(enp1s0) server
            .177                                       .68

2.2 版数

サーバとクライアントのAlmaLinuxの版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 8.6 (Sky Tiger)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
4.18.0-372.9.1.el8.x86_64

クライアントからサーバの11111番ポートにアクセスできるようにするため、TCPの11111番ポートを解放します。なお、firewall-cmdの使い方は、firewall-cmdの使い方は、firewall-cmdの使い方 - hana_shinのLinux技術ブログを参照してください。

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

ルールを確認します。11111番ポートへのアクセスを許可するルールが追加されたことがわかります。

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

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

・サーバ側:ncコマンドを使います。なお、ncコマンドのインストール方法、使い方は、ncコマンドの使い方(ネットワーク実験の幅が広がるなぁ~) - hana_shinのLinux技術ブログを参照してください。
・クライアント側:以下のTPをコンパイルして使用します。なお、見やすくするため、不要な異常処理は意図的に省略しています。

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

int main(int argc, char *argv[])
{
  int sock, n, i;
  struct sockaddr_in server;
  struct timespec req = {0, 20 * 1000000}; //20ms
  struct timeval timeout;
  char buf[4096];

  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.122.68");

  if(atoi(argv[1]) != 0) {
    timeout.tv_sec =atoi(argv[1]);
    timeout.tv_usec = 0;
    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof timeout);
  }

  connect(sock, (struct sockaddr *)&server, sizeof(server));
  for(i=0; i<2000; i++) {
    nanosleep(&req, NULL);
    n = write(sock, buf, sizeof(buf));
    if(n < 0) {
      perror("write");
      close(sock);
      exit(1);
    }
  }
  close(sock);
  exit(0);
}

TPの第1引数の意味は次のとおりです。

第1引数 意味
0 ソケットの送信バッファに空きスペースがなく、プロセスが送信データの書き込みができないと、システムコールの実行が完了しません。つまり、送信バッファに空きスペースができるまで、ブロックしたままになります
0以外 システムコールの実行時間を指定します。ソケットの送信バッファに空きスペースがなく、プロセスが送信データの書き込みができない場合、指定した時間でシステムコールの実行が完了します

TPをコンパイルします。

[root@client ~]# gcc -Wall -o cl cl.c

4 検証結果

サー側でncコマンドを実行します。

[root@server ~]# nc -kl 11111 > /dev/null

4.1 TPの引数に1を指定した場合

クライアント側でTPを実行します。このとき、writeシステムコールの実行時間を確認するため、straceコマンドを使用します。なお、straceコマンドの使い方は、straceコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@client ~]# strace -e trace=write -ttT -f ./cl 1

クライアント側でTPを実行したら、サーバ側ですぐにCtrl+zを押下してncプロセスを停止します。

[root@server ~]# nc -kl 11111 > /dev/null
^Z
[1]+  停止                  nc -kl 11111 > /dev/null

サーバ側でssコマンドの実行結果を確認します。ncプロセスが停止しているため、ソケットの受信バッファから受信データを読み出すことができません。そのため、ソケットの受信バッファに受信データ(220KBほど)が残ったままになっていることがわかります。なお、 Recv-QおよびSend-Qの見方は、ssコマンドのRecv-Q,Send-Qの意味について(ESTABLISHED状態のとき) - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# ss -notp4 'sport == :11111'
State    Recv-Q    Send-Q         Local Address:Port             Peer Address:Port     Process
ESTAB    220232    0             192.168.122.68:11111         192.168.122.177:55924     users:(("nc",pid=1930,fd=4))

クライアント側でssコマンドを実行します。ソケットの送信バッファに送信データ(130KBほど)が残っていることがわかります。これは、サーバ側のncプロセスが受信データを読み出せないため、クライアント側ソケットの送信バッファに送信データが残ったままになっています。

[root@client ~]# ss -notp4 'dport == :11111'
State    Recv-Q    Send-Q          Local Address:Port            Peer Address:Port     Process
ESTAB    0         130320        192.168.122.177:55924         192.168.122.68:11111     users:(("cl",pid=2091,fd=3)) timer:(persist,282ms,1)

クライアント側でTPの実行結果を確認します。しばらくすると、TPが異常終了します。この理由は、ソケットの送信バッファに空きスペースがなく、TPが送信データを送信バッファに書き込むことができずタイムアウトが発生したからです。約1.00秒でwriteシステムコールタイムアウトしていることがわかります。タイムアウトの時間は、TPの引数に指定した1秒におおよそ一致していることがわかります。

[root@client ~]# strace -e trace=write -ttT -f ./cl 1
-snip-
20:30:06.071264 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 272 <1.032207>
20:30:07.124736 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = -1 EAGAIN (リソースが一時的に利用できません) <1.002016>
20:30:08.127457 write(4, "write: Resource temporarily unav"..., 40write: Resource temporarily unavailable
) = 40 <0.000025>
20:30:08.128205 +++ exited with 1 +++

次の検証のため、ncプロセスを再開します。

[root@server ~]# fg
nc -kl 11111 > /dev/null

4.2 TPの引数に2を指定した場合

クライアント側でTPを実行します。

[root@client ~]# strace -e trace=write -ttT -f ./cl 2

クライアント側でTPを実行したら、サーバ側ですぐにCtrl+zを押下してncプロセスを停止します。

[root@server ~]# nc -kl 11111 > /dev/null
^Z
[1]+  停止                  nc -kl 11111 > /dev/null

サーバ側でssコマンドの実行結果を確認します。ncプロセスが停止しているため、ncプロセスがソケットの受信バッファから受信データを読み出せないため、ソケットの受信バッファに受信データ(172KBほど)が残ったままになっていることがわかります

[root@server ~]# ss -notp4 'sport == :11111'
State    Recv-Q    Send-Q         Local Address:Port             Peer Address:Port     Process
ESTAB    172032    0             192.168.122.68:11111         192.168.122.177:55926     users:(("nc",pid=1933,fd=4))

クライアント側でssコマンドを実行します。ソケットの送信バッファに送信データ(130KBほど)が残っていることがわかります。

[root@client ~]# ss -notp4 'dport == :11111'
State    Recv-Q    Send-Q          Local Address:Port            Peer Address:Port     Process
ESTAB    0         130320        192.168.122.177:55926         192.168.122.68:11111     users:(("cl",pid=2099,fd=3)) timer:(persist,892ms,1)

クライアント側でTPの実行結果を確認します。しばらくすると、TPが異常終了します。この理由は、ソケットの送信バッファに空きスペースがなく、TPが送信データを送信バッファに書き込むことができずタイムアウトが発生したからです。約2.03秒でwriteシステムコールタイムアウトしていることがわかります。タイムアウトの時間は、TPの引数に指定した2秒におおよそ一致していることがわかります。

[root@client ~]# strace -e trace=write -ttT -f ./cl 2
-snip-
20:32:45.464058 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 2272 <2.022807>
20:32:47.508146 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = -1 EAGAIN (リソースが一時的に利用できません) <2.026723>
20:32:49.535828 write(4, "write: Resource temporarily unav"..., 40write: Resource temporarily unavailable
) = 40 <0.000020>
20:32:49.536596 +++ exited with 1 +++

次の検証のため、ncプロセスを再開します。

[root@server ~]# fg
nc -kl 11111 > /dev/null

4.3 TPの引数に0を指定した場合

クライアント側でTPを実行します。

[root@client ~]# strace -e trace=write -ttT -f ./cl 0

クライアント側でTPを実行したら、サーバ側ですぐにCtrl+zを押下してncプロセスを停止します。

[root@server ~]# nc -kl 11111 > /dev/null
^Z
[1]+  停止                  nc -kl 11111 > /dev/null

サーバ側でssコマンドの実行結果を確認します。ncプロセスが停止しているため、ncプロセスがソケットの受信バッファから受信データを読み出せないため、ソケットの受信バッファに受信データ(242KBほど)が残ったままになっていることがわかります

[root@server ~]# ss -notp4 'sport == :11111'
State    Recv-Q    Send-Q         Local Address:Port             Peer Address:Port     Process
ESTAB    242808    0             192.168.122.68:11111         192.168.122.177:55928     users:(("nc",pid=1933,fd=4))

クライアント側でssコマンドを実行します。ソケットの送信バッファに送信データ(130KBほど)が残っていることがわかります。

[root@client ~]# ss -notp4 'dport == :11111'
State    Recv-Q    Send-Q          Local Address:Port            Peer Address:Port     Process
ESTAB    0         130320        192.168.122.177:55928         192.168.122.68:11111     users:(("cl",pid=2105,fd=3)) timer:(persist,71582min,0)

クライアント側でTPの実行結果を確認します。ソケットの送信バッファに空きスペースがなくても、writeシステムコールタイムアウトしません。ブロックしたままになります(期待値)。

[root@client ~]# strace -e trace=write -ttT -f ./cl 0
-snip-
20:38:08.973778 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096 <0.000066>
20:38:08.994808 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096 <0.000061>
20:38:09.015844 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096

Z 参考情報

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