こんにちは、オペレーション統括本部(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だけで十分です。
■リーディング-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にしかできないこと>
$ /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
$ jobs
[1]+ Running ./hoge.pl &
$ /bin/kill %1
kill: illegal process id: %1
$ kill %1
[1]+ Terminated ./hoge.pl
$ /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コマンド編)はこちら
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました