2009年5月22日

Tips

ソースコードリーディング(killコマンド編)

  • このエントリーをはてなブックマークに追加

こんにちは、オペレーション統括本部(Yahooショッピング担当)の吉野です。

前回お話しさせていただいたlsコマンドをハックしてみようの公開後、多くの方からご意見をいただきました。
その中で目に付いたのは、「ソースコードリーディングはしてみたいがなかなか(時間|機会)がない」というご意見でした。

そこで、今回はソースコードリーディングとして、FreeBSDで動くkillコマンドのソースコードを読んでみたいと思います。

killコマンドとは、ご存じの通りプロセスにシグナルを送るコマンドです。
trussコマンドでトレースしてみても、killシステムコールしか使っていません。

$ truss /bin/kill 12345
kill(0x1a66,0xf)                                 = 0 (0x0)
exit(0x0)                                       process exit, rval = 0
ソースコードもいたってシンプルなので、簡単に読めると思います。


■ソースコード入手

前回のlsコマンドの記事でご紹介したサイトからダウンロードしましょう。
読むだけであればkill.cだけで十分です。

http://www.freebsd.org/cgi/cvsweb.cgi/src/bin/kill/

■リーディング-1

main()以外で定義されている関数は以下の4つです。

  • static void nosig(const char *);

  • 定義されていないシグナルを指定した場合にエラーメッセージを出力してexit

  • static void printsignals(FILE *);

  • シグナルの一覧を出力する

  • static int signame_to_signum(const char *);

  • シグナル名をシグナル番号に変換する

  • static void usage(void);

  • コマンドの使い方を出力して終了

    main()の中は非常にシンプルです。いくつかポイントを見ていきます。

    ●[コード1] kill.c - main()
    
         69     if (!strcmp(*argv, "-l")) {
         70         argc--, argv++;
         71         if (argc > 1)
         72             usage();
         73         if (argc == 1) {
         74             if (!isdigit(**argv))
         75                 usage();
         76             numsig = strtol(*argv, &ep, 10);
         77             if (!**argv || *ep)
         78                 errx(1, "illegal signal number: %s", *argv);
         79             if (numsig >= 128)
         80                 numsig -= 128; 
         81             if (numsig <= 0 || numsig >= sys_nsig)
         82                 nosig(*argv);
         83             printf("%s\n", sys_signame[numsig]);
         84             exit(0);
         85         }
         86         printsignals(stdout);
         87         exit(0);
         88     }

    [コード1]はオプションで-lが指定されたときの処理です。
    -lの後に引数がない場合はprintsignals()が呼び出され、定義されている全シグナルが出力されます。
    -lの後に引数がある場合は73行目のifに入り、数値かどうか/定義内の数値かどうか などの判定が行われ、
    83行目で指定されたシグナル番号のシグナル名を出力します。
    また、74行目でisdigitの判定をしているため、シグナル名->シグナル番号の変換はされないことがわかります。

    79,80行目で「128以上の場合は-128する」という処理が入っています。

    $ /bin/kill -l 129             <-- 129のシグナル番号は存在しないが
    hup                            <-- -128され、シグナル番号1のhupが出力される
    $ /bin/kill -l 159
    usr2
    $ /bin/kill -l 160             <-- 160-128=32のためエラーとなる
    kill: unknown signal 160; valid signals:
    hup int quit ill trap abrt emt fpe kill bus segv sys pipe alrm term urg
    stop tstp cont chld ttin ttou io xcpu xfsz vtalrm prof winch info usr1 usr2

    この仕様はおそらく、シェルが異常終了した場合終了ステータスが「128+シグナル番号」になることに由来しているのだと思われます。
    (後述するbash版killのソースにもコメントがあります)

    ●[コード2] kill.c - main()
    
         96         if (strcmp(*argv, "0")) {
         97             if ((numsig = signame_to_signum(*argv)) < 0)
         98                 nosig(*argv);
         99         } else
    ●[コード3] kill.c - signame_to_signum()
    
        143     if (!strncasecmp(sig, "sig", (size_t)3))
        144         sig += 3;
        145     for (n = 1; n < sys_nsig; n++) {
        146         if (!strcasecmp(sys_signame[n], sig))
        147             return (n);
        148     }

    [コード2]は-sオプション時の処理の一部です。signame_to_signum()でシグナル名->シグナル番号に変換します。
    [コード3]のsigname_to_signum()内では、143,144行目で先頭にsigが付いていた場合は3文字分読み飛ばす処理が記述されています。
    sys_nsigはシグナルの個数が入っており、145行目のforで全シグナル数分回します。

    sys_signameはシグナル名が格納されている配列で、配列の添字=シグナル番号 となっています。
    これを大文字小文字無視のstrcasecmpで引数となったシグナル名とマッチさせて添字を返却しています。
    そのため、以下のコマンドは全て同じ結果となります。

    $ /bin/kill -s KILL 12345
    $ /bin/kill -s kill 12345
    $ /bin/kill -s SIGKILL 12345
    $ /bin/kill -s sigkill 12345
    ●[コード4] kill.c - main()
    
        102     } else if (**argv == '-' && *(*argv + 1) != '-') {
        103         ++*argv;
        104         if (isalpha(**argv)) {
        105             if ((numsig = signame_to_signum(*argv)) < 0)
        106                 nosig(*argv);
        107         } else if (isdigit(**argv)) {
        108             numsig = strtol(*argv, &ep, 10);
        109             if (!**argv || *ep)
        110                 errx(1, "illegal signal number: %s", *argv);
        111             if (numsig < 0 || numsig >= sys_nsig)
        112                 nosig(*argv); 
        113         } else  
        114             nosig(*argv);

    [コード4]は オプションが -[シグナル番号] -[シグナル名] の時の処理です。
    107〜112行目は -[シグナル番号] の処理ですが、[コード1]にあった-128の処理がありません。

    /bin/kill -129 12345             <-- 実際のkillの処理では-128されない
    kill: unknown signal 129; valid signals:
    hup int quit ill trap abrt emt fpe kill bus segv sys pipe alrm term urg
    stop tstp cont chld ttin ttou io xcpu xfsz vtalrm prof winch info usr1 usr2
    ●[コード5] kill.c - main()
    
        124     for (errors = 0; argc; argc--, argv++) {
        125         pid = strtol(*argv, &ep, 10);
        126         if (!**argv || *ep) {
        127             warnx("illegal process id: %s", *argv);
        128             errors = 1;
        129         } else if (kill(pid, numsig) == -1) {
        130             warn("%s", *argv);
        131             errors = 1;
        132         }
        133     }

    [コード5]は、最終的にプロセス番号順にループして順番にkillシステムコールを使用してkillしています。

    ■シェル組み込みコマンドのkill

    ここまでは/bin/killのソースコードを読んできましたが、killを組み込みコマンドとして用意しているシェルもあります。
    bashやtcsh使いの方は、普段はシェル組み込みコマンドのkillを使われているのではないでしょうか。
    (通常は/bin/killと明示的しない限りは、シェル組み込みのkillを使用します)

    /bin/killだけではちょっと物足りないので、bash版killも少し見てみましょう。


    ■機能の違い

    まずは機能の違いを比べてみましょう。
    usegeの出力を見るだけでも、微妙に違うことがわかります。

    $ kill
    kill: usage: kill [-s sigspec | -n signum | -sigspec] [pid | job]... or kill -l [sigspec]
    $ /bin/kill
    usage: kill [-s signal_name] pid ...
           kill -l [exit_status]
           kill -signal_name pid ...
           kill -signal_number pid ...

    <bash版killにしかできないこと>

  • -sでシグナル番号の指定ができる
  • $ /bin/kill -s 15 12345
    kill: unknown signal 15; valid signals:
    hup int quit ill trap abrt emt fpe kill bus segv sys pipe alrm term urg
    stop tstp cont chld ttin ttou io xcpu xfsz vtalrm prof winch info usr1 usr2
    $ kill -s 15 12345

  • pid指定の他にジョブID指定ができる
  • $ jobs
    [1]+  Running                 ./hoge.pl &
    $ /bin/kill %1
    kill: illegal process id: %1
    $ kill %1
    [1]+  Terminated              ./hoge.pl

  • -lでシグナル名の指定ができる
  • $ /bin/kill -l 1
    hup
    $ /bin/kill -l hup
    usage: kill [-s signal_name] pid ...
           kill -l [exit_status]
           kill -signal_name pid ...
           kill -signal_number pid ...
    $ kill -l 1
    HUP
    $ kill -l hup
    1

    など。。 bash版killの方が高機能に作られていて、そのぶんソースコードも少し複雑になっています。
    また、変わったスタイルで書かれていますので、正直ちょっと読みづらいです。


    ■bash版killソースコード入手

    bash版killはシェル組み込みコマンドのため、ソースコードはbashのパッケージの中に入っています。 http://www.gnu.org/software/bash/bash.html
    FTPのサイトに入り、bash-4.0.tar.gz をダウンロードします。
    killのメインソースは bash-4.0/builtins/kill.def です。

    ■リーディング-2

    メイン処理はほとんどkill.defのkill_builtin()に記述されていて、一部の処理はcommon.c trap.c jobs.cに記述されています。

    ●[コード6] kill.def - kill_builtin()
    
        108       if (ISOPTION (word, 'l'))
        109         {
        110           listing++;
        111           list = list->next;
        112         }
    
        155   if (listing)
        156     return (display_signal_list (list, 0));
    ●[コード7] common.c - display_signal_list()
    
        706       for (i = 1, column = 0; i < NSIG; i++)
        707         {
        708           name = signal_name (i);
        709           if (STREQN (name, "SIGJUNK", 7) || STREQN (name, "Unknown", 7))
        710             continue;
        711 
        712           if (posixly_correct && !forcecols)
        713             {
        714               /* This is for the kill builtin.  POSIX.2 says the signal names
        715                  are displayed without the `SIG' prefix. */
        716               if (STREQN (name, "SIG", 3))
        717                 name += 3;
        718               printf ("%s%s", name, (i == NSIG - 1) ? "" : " ");
        719             }
        720           else
        721             {
        722               printf ("%2d) %s", i, name);
        723 
        724               if (++column < 5)
        725                 printf ("\t");
        726               else
        727                 {
        728                   printf ("\n");
        729                   column = 0;
        730                 }
        731             }
        732         }
    ●[コード8] common.c - display_signal_list()
    
        744           /* This is specified by Posix.2 so that exit statuses can be
        745              mapped into signal numbers. */
        746           if (lsignum > 128)
        747             lsignum -= 128;
    
        776           signum = decode_signal (list->word->word, dflags);

    [コード6][コード7][コード8]は-lオプション時の処理の一部です。
    [コード7]708行目のsignal_name()でシグナル番号からシグナル名を取得しています。signal_name() bash-4.0/trap.c に定義されています。
    また、724〜730行目で、1行の出力コラム数を4で改行していることがわかります。
    [コード8]では、/bin/killと同じように「128より大きい数値の場合は-128する」という記述がされています。(">="と"="の違いはありますが)
    この処理のコメントでは、「終了ステータスをシグナル番号にマッピングできるようにPosix.2で規定されている」と書かれています。
    /bin/killのusageに「kill -l [exit_status]」と書かれているのはその為でしょうか。

    また、776行目はシグナル名が指定されたときの処理で、decode_signal()でシグナル番号に変換していることがわかります。
    decode_signal()は、bash-4.0/trap.c に記述されていますが、やっていることは/bin/killのsigname_to_signum()とあまり変わりません。

    ●[コード9] kill.def - kill_builtin()
    
        203           int job;
        204           sigset_t set, oset;
        205           JOB *j;
        206 
        207           BLOCK_CHILD (set, oset);
        208           job = get_job_spec (list);
        209 
        210           if (INVALID_JOB (job))
        211             {
        212               if (job != DUP_JOB)
        213                 sh_badjob (list->word->word);
        214               UNBLOCK_CHILD (oset);
        215               CONTINUE_OR_FAIL;
        216             }
        217 
        218           j = get_job_by_jid (job);
        219           /* Job spec used.  Kill the process group. If the job was started
        220              without job control, then its pgrp == shell_pgrp, so we have
        221              to be careful.  We take the pid of the first job in the pipeline
        222              in that case. */
        223           pid = IS_JOBCONTROL (job) ? j->pgrp : j->pipe->pid;
    ●[コード10] common.c - get_job_spec()
    
        662   if (DIGIT (*word) && all_digits (word))
        663     {
        664       job = atoi (word);
        665       return (job > js.j_jobslots ? NO_JOB : job - 1);
        666     }
        667 
        668   jflags = 0;
        669   switch (*word)
        670     {
        671     case 0:
        672     case '%':
        673     case '+':
        674       return (js.j_current);
        675 
        676     case '-':
        677       return (js.j_previous);
        678 
        679     case '?':                   /* Substring search requested. */
        680       jflags |= JM_SUBSTRING;
        681       word++;
        682       /* FALLTHROUGH */
        683 
        684     default:
        685       return get_job_by_name (word, jflags);
        686     }

    [コード9][コード10]はジョブIDを指定したときの処理の一部です。
    [コード9]208行目のget_job_spec()でジョブIDを割り出し、218行目のget_job_by_jid()でJOBオブジェクトを生成し、
    223行目でジョブのプロセスグループIDを取得しています。
    [コード10]では、該当するジョブIDを返却しています。%% %+ でカレントジョブ・%-でカレントの前のジョブIDが指定できることがわかります。

    ●[コード11] kill.def - kill_builtin()
    
        181           pid = (pid_t) pid_value;
        182             
        183           if (kill_pid (pid, sig, pid < -1) < 0)
        184             { 
        185               if (errno == EINVAL)
        186                 sh_invalidsig (sigspec);
        187               else
        188                 kill_error (pid, errno);
        189               CONTINUE_OR_FAIL;
        190             }
        191           else
        192             any_succeeded++;
    
        227           if (kill_pid (pid, sig, 1) < 0)
        228             {
        229               if (errno == EINVAL)
        230                 sh_invalidsig (sigspec);
        231               else
        232                 kill_error (pid, errno);
        233               CONTINUE_OR_FAIL;
        234             }
        235           else
        236             any_succeeded++;

    [コード11]は、実際にプロセスをkillしている部分です。上部分がプロセスID指定時・下部分がジョブID指定時の処理です。
    どちらもkill_pid()を使用してkillしていることがわかります。(kill_pid()は bash-4.0/jobs.c に記述されています)
    kill_pid()の中では、killシステムコール、killpgシステムコールを使用しています。


    ■まとめ

    /bin/killがかなりあっさりしていたのでbash版killも読んでみましたが、思ったより深入りしてしまいました。
    今回は違う部分だけを見せるためにかなり絞ったため、少々見づらくなってしまったかもしれません。

    killに関連して、/usr/bin/killallについても書きたかったのですが、長くなりそうなので本日はこの辺で終了します。

    ※現在ソースコードリーディングのシリーズ化を検討しています(評判が良ければ、ですが;)。
    ご覧になった皆様からご意見をいただけるとありがたいです。

    今回のお話は以上です。
    またお会いしましょう。

    2009/06/17追記>>
    lsコマンドをハックしてみようはこちら
    ソースコードリーディング(head,tailコマンド編)はこちら

    Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

    • このエントリーをはてなブックマークに追加