🧾

シェルスクリプトでstraceとtcpdumpを取るときのTIPS

2021/11/25に公開

curl から nginx への HTTP の通信を tcpdump でパケットキャプチャーしつつ、
strace で nginx のシステムコール呼び出しをトレースしたいということがあって
シェルスクリプトを書きました。

その時に試行錯誤していくつか TIPS が出来たのでメモしておきます。

nginx のように複数プロセスの場合は strace の -ff と -o が便利

strace (1) に書いてある話ですが、つい最近まで知らなかったので書いておきます。

strace -ff -o ファイル名 straceを取りたいコマンド コマンドの引数...

のように実行すると、対象のコマンドがサブプロセスを起動したらそれも合わせて strace を取ってくれます。その際、ファイル名は指定したファイル名のあとに . とPIDをつけたファイル名になります。

例えば以下のように実行します。

strace -ff -o nginx-strace.log /usr/sbin/nginx -g 'daemon off;'

それで例えばマスタープロセスのPIDが 2022143、ワーカープロセスが1つでPIDが2022144だとすると nginx-strace.log.2022143 と nginx-strace.log.2022144 という2つのファイルが作られるというわけです。

tcpdump の -w でファイルに書くときは -U も指定するほうが良い場合がある

こちらも tcpdump (8)-U --packet-buffered に書いてますが、知らなくてハマったので紹介します。

tcpdump で -w ファイル名 と指定するとダンプ結果をファイルに出力しますが、その場合バッファリングされるようになります。するとキャプチャーしたい通信が終わった後 Ctrl-C や kill で tcpdump を止めてもバッファされていた部分がファイルに書かれないという問題があります。

以前これを知らずに、通信後数秒待ってから tcpdump を止めるようにしてみたけど相変わらず最後のほうのパケットが出ないなと悩んでいたのですがバッファリングされていたからでした。

-U も指定するとバッファリングせずに都度ファイルに出力するのでその問題がなくなります。大量の通信をキャプチャーする場合は -U はつけないほうが良いと思いますが、curl で1リクエストとかの場合は -U を付けたほうが良いです。

例えば strace を指定しつつ nginx を起動して、 tcpdump でパケットをキャプチャーする場合は以下のようにします。

strace -ff -o $log_dir/nginx-strace.log nginx -g 'daemon off;' &

tcpdump -i any -U -w "$log_dir/tcpdump.bin.log" tcp port 80 &

複数起動したプロセスをまとめて kill するにはプロセスグループを使うのが便利

上記のように複数のプロセスを起動して、検証用の通信が終わったらまとめて止めたいという場合、各プロセスのPIDを調べて kill するのは面倒です。

kill (1) でPIDのところに負の値でプロセスグループのIDを指定すると、そのプロセスグループ内の全プロセスを kill してくれることを知りました。

今のプロセスのプロセスグループIDは以下のコマンドで取得可能です。

pgid=$(ps -p $$ -o pgid --no-headers)

プロセスグループを kill するには

kill -- -$pgid

kill -TERM -$pgid

と実行します。シグナルを数値で指定するときは -15 となってプロセスグループIDを符号反転させたものと区別がつかないので、シグナルを省略する場合は -- で区切るか、あるいは最初にシグナルのオプションを省略せずに書く必要があります。

で喜んで実行していたら、スクリプトを実行していたシェルのプロンプトごと kill されてしまいました。これは単にシェルスクリプトを起動する場合はシェルのプロンプトのプロセスグループと同じプロセスグループ内で実行されるからです。

検索したら setsid (1) というコマンドがありました。これを使えば別のプロセスグループを作ってその中でスクリプトを実行することができます。

実際に使うときはシェルスクリプトを2つに分けて以下のような感じにしました。

run.sh

#!/bin/bash
usage() {
  cat <<EOF >&2
Usage: $0 test_case
test_case must be integer between 1 and 3.
EOF
  exit 2
}

if [ $# -ne 1 ]; then usage; fi
if [ "$1" -lt 1 ] || [ "$1" -gt 3 ]; then usage; fi

test_case=$1
log_dir=log-testcase$test_case

mkdir $log_dir
sudo setsid ./do_run.sh $test_case $log_dir 2>&1 | tee $log_dir/do_run.log
sudo chown -R $USER: $log_dir
tcpdump -A -n -vvv -r "$log_dir/tcpdump.bin.log" > "$log_dir/tcpdump.log"

do_run.sh

#!/bin/bash
test_case=$1
log_dir=$2

set -e
set -x
strace -ff -o $log_dir/nginx-strace.log nginx -g 'daemon off;' &

tcpdump -i any -U -w "$log_dir/tcpdump.bin.log" tcp port 80 &

sleep 2
pgid=$(ps -p $$ -o pgid --no-headers)
ps -efj | awk 'NR == 1 || ($4 == '$pgid' && $10 == "nginx:") {print}'

case $test_case in
1) strace -o $log_dir/curl-strace.log curl -sSv http://localhost/ http://localhost/ > $log_dir/curl.log 2>&1 ;;
2) strace -o $log_dir/curl-strace.log curl -sSv --no-keepalive $curl_ka http://localhost/ http://localhost/ > $log_dir/curl.log 2>&1 ;;
3) strace -o $log_dir/curl-strace.log curl -sSv -H 'User-Agent: MSIE 6.0' -X POST http://localhost/ http://localhost/ > $log_dir/curl.log 2>&1 ;;
esac

sleep 2
kill -- -$pgid

run.sh から setsid を使って do_run.sh を起動し、 do_run.sh の最後で kill で do_run.sh のプロセスグループ全体を kill しています。
これで do_run.sh 自身とその中で起動したバックグラウンドプロセスをまとめて kill できるというわけです。

Discussion