Help us understand the problem. What is going on with this article?

signal送信元を知る

はじめに

プロセス間の非同期通信としてよく使われるsignalですが、
送信元を知る必要があり方法を調べてみたので備忘メモとして残しておきます。

試した環境は以下のとおりです。

$ lsb_release -d
Description:    Ubuntu 18.04.4 LTS
$ uname -r
5.3.0-61-generic
$ trace-cmd --version | grep version
trace-cmd version 2.6.1
$ stap --version | head -1
Systemtap translator/driver (version 4.3/0.170, commit release-4.3-0-gc9c23c987d81)

方法1. sigaction(2)で知る

signalを受け取るプロセスで送信元の情報を取得するアプローチです。

signalを受け取るプロセスを変更することができて、
かつプロセスでハンドリングできるsignalが調査対象の場合に使える方法です。
SIGKILLのようにプロセスでハンドリングできないsignalには使えません。

sigaction(2)は、sa_flagsSA_SIGINFOを指定することで、
シグナルハンドラでsignalに関する詳細な情報siginfo_tを受け取ることができるようになります。
si_pidで送信元のPIDを、si_uidで送信元の実ユーザIDを知ることができます。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

static int g_sig;
static siginfo_t g_siginfo;

static void
my_sigaction (int sig, siginfo_t *info, void *ucontext)
{
  g_sig = sig;
  // memcpy(3) is a async-signal-safe function
  // according to man signal-safety(7)
  memcpy (&g_siginfo, info, sizeof(g_siginfo));
}

static char*
code2str (int code)
{
  switch (code)
    {
    case SI_USER: return "SI_USER";
    case SI_KERNEL: return "SI_KERNEL";
    case SI_QUEUE: return "SI_QUEUE";
    case SI_TIMER: return "SI_TIMER";
    case SI_MESGQ: return "SI_MESGQ";
    case SI_ASYNCIO: return "SI_ASYNCIO";
    case SI_SIGIO: return "SI_SIGIO";
    case SI_TKILL: return "SI_TKILL";
    default: return "unknown";
    }
}

int
main (void)
{
  struct sigaction act;
  act.sa_flags = SA_SIGINFO;
  act.sa_sigaction = my_sigaction;
  int ret = sigaction(SIGTERM, &act, NULL);
  if (ret < 0)
    {
      perror ("sigaction");
      exit (EXIT_FAILURE);
    }
  printf ("pid: %d\n", getpid());

  sleep (10000);

  fprintf (stderr, "sig: %d, si_pid: %d, si_uid: %d, si_code: %s\n",
           g_sig,
           g_siginfo.si_pid,
           g_siginfo.si_uid,
           code2str(g_siginfo.si_code));

  return 0;
}

方法2.trace-cmd (=ftrace)で知る

kernelは誰が誰にsignalを送ったか知っているので、kernelのイベントをトレースすることで調べるアプローチです。
kernelのイベントトレースには、ftraceを使います。

ftraceでsignal_generateのイベントをトレースすることで送信元を特定することができます。
debugfsからftraceを直接操作してもいいですが、ここではtrace-cmdを使ってトレースする方法を紹介します。

[トレース開始]
$ sudo trace-cmd start -e signal_generate -f 'sig==9 && pid == 6985'

[トレース終了]
$ sudo trace-cmd stop

[トレース結果の確認]
$ sudo trace-cmd show
# tracer: nop
#
# entries-in-buffer/entries-written: 1/1   #P:8
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
     test_sender-6993  [001] d...  1660.447368: signal_generate: sig=9 errno=0 code=0 comm=test_receiver pid=6985 grp=1 res=0

pid=6985のプロセスにsig=9を送ったのは、test_senderというPIDが6993のプロセスであることがわかります。

ここでsignal_generateイベントは大量に発生するので、上記のように-fオプションを使い適切にフィルタをかけたほうが良いです。
上記では、signal番号とsignalを受け取るpidでフィルタしています。

フィルタ関数で使えるfield名は、trace-cmd listコマンドで確認できます。

$ trace-cmd list -F -e signal_generate
system: signal
name: signal_generate
ID: 194
format:
    field:unsigned short common_type;   offset:0;   size:2; signed:0;
    field:unsigned char common_flags;   offset:2;   size:1; signed:0;
    field:unsigned char common_preempt_count;   offset:3;   size:1; signed:0;
    field:int common_pid;   offset:4;   size:4; signed:1;

    field:int sig;  offset:8;   size:4; signed:1;
    field:int errno;    offset:12;  size:4; signed:1;
    field:int code; offset:16;  size:4; signed:1;
    field:char comm[16];    offset:20;  size:16;    signed:1;
    field:pid_t pid;    offset:36;  size:4; signed:1;
    field:int group;    offset:40;  size:4; signed:1;
    field:int result;   offset:44;  size:4; signed:1;

方法3.systemtapで知る

方法2と同じく、kernelのイベントをトレースすることで調べるアプローチです。
kernelのイベントトレースにsystemtapを使います。

下記のようにワンライナーでモニタできます。

$ stap -e 'probe signal.send { if (sig==9 && sig_pid==8722) printf("%s: %s(%d) -> %s(%d)\n", sig_name, execname(), pid(), pid_name, sig_pid) }'
SIGKILL: test_sender(9191) -> test_receiver(8722)

スクリプトにして、ついでに引数取れるようにすると以下のようになります。
(qiitaにsystemtapモードがないようなので、c言語モードにしています)

signal_sender.stp
#!/usr/bin/env stap
probe signal.send
{
  if (sig==strtol(@1,10) && sig_pid==strtol(@2,10))
    printf("%s: %s(%d) -> %s(%d)\n", sig_name, execname(), pid(), pid_name, sig_pid)
}

systemtapはPCだとこのようにスマートにできるのですが、
組み込みだとコンパイル環境と実行環境が別になるため手順が少し煩雑になるのが欠点です。

まとめ

他にもあると思いますが、選択肢として3つ紹介しました。
調べた中にはSELinuxのaudit.logを利用するなんて方法もありました(ご興味があればこちらをご覧ください)。
個人的には、使いやすさと環境のハードルのバランスがいいのは、ftrace(trace-cmd)かなと思ってます。
ちょっと力尽きてしまいましたが、eBPFでもできるでしょうからeBPF版もそのうち追記したいです。

参考

Man page of SIGACTION
trace-cmd(1) - Linux man page
stap(1): systemtap script translator/driver - Linux man page
trace-cmdコマンドの使い方 - Qiita
プロセスへの SIGKILL の送信元を追跡する方法 - Red Hat Customer Portal
takeoverjp/test_signal: signal simple test program

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした