こんにちは、みなさんお久しぶりです。Yahoo!ショッピング開発担当の吉野哲仁です。
約1年3ヶ月ぶりのソースコードリーディングは、wコマンドのソースコードを読んでいきたいと思います。
■はじめに
wコマンドは、システムの稼働状況とログインユーザーの情報を表示するコマンドです。
システム運用ではよく使われます。ロードアベレージを取得するために使用する場合も多いと思います。
wコマンドでやっていることは大きく分けて2つ。
・現在のシステム状況(システム稼働時間、ロードアベレージ)の表示
・現在のログインユーザー情報(ユーザー名、端末名、ログイン元ホスト、現在実行しているコマンドなど)の表示
です。今回はこの2つにスポットを当てて読んでいきたいと思います。
※ソースのリビジョンについて
今回読むソースのリビジョンは、カレントの最新(9.0)ではなく、「RELENG_8_1_0_RELEASE」のcvsタグが付いているリビジョンです。
(w.cの場合はリビジョン 1.60.10.3.2.1 です)
w.cなどは最新とは若干処理が異なります(ログインユーザー情報の取得方法など)。
<src/usr.bin/w>
http://www.freebsd.org/cgi/cvsweb.cgi/src/usr.bin/w/
■1.現在のシステム状況の表示
まずは実際の出力を見てみましょう。
●[コマンド1] wコマンド結果
$ w
9:26AM up 311 days, 15:34, 4 users, load averages: 0.46, 0.33, 0.30
USER TTY FROM LOGIN@ IDLE WHAT
user1 p0 111.111.111.111 30 810 - vi hoge
user2 p1 111.111.111.112 01 910 - w
user3 p2 111.111.111.113 02 910 3days -bash (bash)
user4 p3 111.111.111.114 月08PM 4days -bash (bash)
こんな感じで、システム稼働時間/ロードアベレージ/ログインユーザー情報などが出力されます。
この章では、
9:26AM up 311 days, 15:34, 4 users, load averages: 0.46, 0.33, 0.30
この部分を見ていきます。
まず、ログインユーザー数の取得です。
●[コード1] w.c - main()
212 if ((ut = fopen(_PATH_UTMP, "r")) == NULL)
213 err(1, "%s", _PATH_UTMP);
214
215 if (*argv)
216 sel_users = argv;
217
218 for (nusers = 0; fread(&utmp, sizeof(utmp), 1, ut);) {
219 if (utmp.ut_name[0] == '\0')
220 continue;
221 if (!(stp = ttystat(utmp.ut_line, UT_LINESIZE)))
222 continue; /* corrupted record */
223 ++nusers;
224 if (wcmd == 0)
225 continue;
226 if (sel_users) {
227 int usermatch;
228 char **user;
229
230 usermatch = 0;
231 for (user = sel_users; !usermatch && *user; user++)
232 if (!strncmp(utmp.ut_name, *user, UT_NAMESIZE))
233 usermatch = 1;
234 if (!usermatch)
235 continue;
236 }
237 if ((ep = calloc(1, sizeof(struct entry))) == NULL)
238 errx(1, "calloc");
239 *nextp = ep;
240 nextp = &ep->next;
241 memmove(&ep->utmp, &utmp, sizeof(struct utmp));
242 ep->tdev = stp->st_rdev;
243 /*
244 * If this is the console device, attempt to ascertain
245 * the true console device dev_t.
246 */
247 if (ep->tdev == 0) {
248 size_t size;
249
250 size = sizeof(dev_t);
251 (void)sysctlbyname("machdep.consdev", &ep->tdev, &size, NULL, 0);
252 }
253 touched = stp->st_atime;
254 if (touched < ep->utmp.ut_time) {
255 /* tty untouched since before login */
256 touched = ep->utmp.ut_time;
257 }
258 if ((ep->idle = now - touched) < 0)
259 ep->idle = 0;
260 }
261 (void)fclose(ut);
●[コード2] w.c
110 struct entry {
111 struct entry *next;
112 struct utmp utmp;
113 dev_t tdev; /* dev_t of terminal */
114 time_t idle; /* idle time of terminal in seconds */
115 struct kinfo_proc *kp; /* `most interesting' proc */
116 char *args; /* arg list of interesting process */
117 struct kinfo_proc *dkp; /* debug option proc list */
118 } *ep, *ehead = NULL, **nextp = &ehead;
[コード1]はutmpファイルから現在ログインしているユーザー情報の取得を行っています。218行目から出てくる nusers がユーザー数をカウントする変数です。
ユーザー数をカウントするという意味では223行目の ++nusers; だけなのですが、後ほど読んでいくログインユーザー情報の表示部分で使用する処理がありますので、ついでに読んでいきます。
221行目では、ユーザーの端末デバイスファイル(/dev/ttyp*)のファイルステータス情報を取得しています。
221行目の ttystat() は w.c 内に定義されている関数で、中ではstatシステムコールを使用して、端末デバイスファイルのstat構造体を取得しています。stpはstat構造体へのポインタです。
237行目から出てくるepは、[コード2]にあるentry構造体のポインタです。最終的にはこのepの中にログインユーザー情報がすべて入ります。epはこの後も頻繁に登場するので覚えてください。
242行目では ep->tdev にユーザーの端末ID(ttyp1のp1の部分)を stp->st_rdev から取得しています。
253~259行目では、端末のアイドル時間を取得しています。端末のデバイスファイル(/dev/ttyp*)からatime(最終アクセス時間)を取得(stp->st_atime)し、「現在の時間 - 最終アクセス時間」の差分をアイドル時間として ep->idle に代入しています。
●[コード3] w.c - pr_header()
434 /*
435 * Print how long system has been up.
436 */
437 if (clock_gettime(CLOCK_MONOTONIC, &tp) != -1) {
438 uptime = tp.tv_sec;
439 if (uptime > 60)
440 uptime += 30;
441 days = uptime / 86400;
442 uptime %= 86400;
443 hrs = uptime / 3600;
444 uptime %= 3600;
445 mins = uptime / 60;
446 secs = uptime % 60;
447 (void)printf(" up");
448 if (days > 0)
449 (void)printf(" %d day%s,", days, days > 1 ? "s" : "");
450 if (hrs > 0 && mins > 0)
451 (void)printf(" %2d:%02d,", hrs, mins);
452 else if (hrs > 0)
453 (void)printf(" %d hr%s,", hrs, hrs > 1 ? "s" : "");
454 else if (mins > 0)
455 (void)printf(" %d min%s,", mins, mins > 1 ? "s" : "");
456 else
457 (void)printf(" %d sec%s,", secs, secs > 1 ? "s" : "");
458 }
[コード3]ではマシンの稼働時間を取得して表示しています。稼働時間の取得にはclock_gettime()システムコールを使用し、「up 311 days, 15:34」といった形に整形して表示しています。
●[コード4] w.c - pr_header()
460 /* Print number of users logged in to system */
461 (void)printf(" %d user%s", nusers, nusers == 1 ? "" : "s");
462
463 /*
464 * Print 1, 5, and 15 minute load averages.
465 */
466 if (getloadavg(avenrun, sizeof(avenrun) / sizeof(avenrun[0])) == -1)
467 (void)printf(", no load average information available\n");
468 else {
469 (void)printf(", load averages:");
470 for (i = 0; i < (int)(sizeof(avenrun) / sizeof(avenrun[0])); i++) {
471 if (use_comma && i > 0)
472 (void)printf(",");
473 (void)printf(" %.2f", avenrun[i]);
474 }
475 (void)printf("\n");
476 }
[コード4]では、ログインユーザー数の表示とロードアベレージの取得/表示をしています。461行目でログインユーザー数(nusers)の表示、466行目でgetloadavg()を使用してロードアベレージの取得を行っています。
avenrun の定義は「double avenrun[3];」です。getloadavg()の結果、
・avenrun[0] - 1分のロードアベレージ
・avenrun[1] - 5分のロードアベレージ
・avenrun[2] - 15分のロードアベレージ
といった感じでロードアベレージが取得できます。
getloadavg()は標準ライブラリ関数ですが、今回は少しgetloadavg()の中身も読んでみましょう。
●[コード5] src/lib/libc/gen/getloadavg.c - getloadavg()
50 int
51 getloadavg(loadavg, nelem)
52 double loadavg[];
53 int nelem;
54 {
55 struct loadavg loadinfo;
56 int i, mib[2];
57 size_t size;
58
59 mib[0] = CTL_VM;
60 mib[1] = VM_LOADAVG;
61 size = sizeof(loadinfo);
62 if (sysctl(mib, 2, &loadinfo, &size, NULL, 0) < 0)
63 return (-1);
64
65 nelem = MIN(nelem, sizeof(loadinfo.ldavg) / sizeof(fixpt_t));
66 for (i = 0; i < nelem; i++)
67 loadavg[i] = (double) loadinfo.ldavg[i] / loadinfo.fscale;
68 return (nelem);
69 }
[コード5]はgetloadavg()の全コードです。55行目のloadavg構造体の定義は src/sys/sys/resource.h にあります。
ロードアベレージはsysctl()を使用して取得しています。sysctl()はシステム情報を取得する際に頻繁に使用されるライブラリ関数で、59,60行目にあるとおり、sysctl()の第1引数には取得する情報名を配列で指定しています。第1レベルにCTL_VM、第2レベルにVM_LOADAVG を指定することでロードアベレージが取得できます。
sysctl()の詳細については機会があったら別の回で読んでいきたいと思います。
■2.現在のログインユーザー情報の表示
続いて、現在ログインしているユーザー情報を取得する部分を見ていきます。[コマンド1]の
USER TTY FROM LOGIN@ IDLE WHAT
user1 p0 111.111.111.111 30 810 - vi hoge
user2 p1 111.111.111.112 01 910 - w
user3 p2 111.111.111.113 02 910 3days -bash (bash)
user4 p3 111.111.111.114 月08PM 4days -bash (bash)
この部分です。
[コード1]で、すでにutmpから端末IDやアイドル時間の取得を行いました。次はユーザーの実行プロセスの取得部分です。
●[コード6] w.c - main()
284 if ((kp = kvm_getprocs(kd, KERN_PROC_ALL, 0, &nentries)) == NULL)
285 err(1, "%s", kvm_geterr(kd));
286 for (i = 0; i < nentries; i++, kp++) {
287 if (kp->ki_stat == SIDL || kp->ki_stat == SZOMB)
288 continue;
289 for (ep = ehead; ep != NULL; ep = ep->next) {
290 if (ep->tdev == kp->ki_tdev) {
291 /*
292 * proc is associated with this terminal
293 */
294 if (ep->kp == NULL && kp->ki_pgid == kp->ki_tpgid) {
295 /*
296 * Proc is 'most interesting'
297 */
298 if (proc_compare(ep->kp, kp))
299 ep->kp = kp;
300 }
301 /*
302 * Proc debug option info; add to debug
303 * list using kinfo_proc ki_spare[0]
304 * as next pointer; ptr to ptr avoids the
305 * ptr = long assumption.
306 */
307 dkp = ep->dkp;
308 ep->dkp = kp;
309 debugproc(kp) = dkp;
310 }
311 }
312 }
●[コード7] w.c - main()
322 for (ep = ehead; ep != NULL; ep = ep->next) {
323 if (ep->kp == NULL) {
324 ep->args = strdup("-");
325 continue;
326 }
327 ep->args = fmt_argv(kvm_getargv(kd, ep->kp, argwidth),
328 ep->kp->ki_comm, MAXCOMLEN);
329 if (ep->args == NULL)
330 err(1, NULL);
331 }
[コード6]はユーザーの実行プロセスを特定する部分です。
まず、284行目でkvm_getprocs()を使って、すべてのプロセス情報を取得します(kvm_getprocs()に渡すkd[カーネル仮想メモリ領域へのディスクリプタ]を取得するために、208行目でkvm_openfiles()を使っています)。kvm_getprocs()の結果取得される構造体(kinfo_proc)は、src/sys/sys/user.h に定義されています。
続いて286行目からプロセスの個数(nentries)分ループし、ep(現在のログインユーザー情報)の内容と比較してユーザーの現在の実行プロセスを特定します。
290行目でepに格納されている端末ID(ep->tdev)とプロセスの端末ID(kp->ki_tdev)を比較して、同じ端末IDから実行されたプロセスを特定します。
さらに294行目でプロセスのグループID(kp->ki_pgid)と、端末のプロセスグループID(kp->ki_tpgid)を比較し、一致していたらそのプロセスは現在端末で実行されているプロセスとみなします。
307行目~309行目は、-dオプションが指定された時用に、その端末で実行されているプロセスをすべて ep->dkp に格納しています。
[コード7]では、特定したプロセスのコマンドラインを引数を含めてフルで取得し、ep->argsに格納しています。327行目のfmt_argv()は src/bin/ps/fmt.c に定義されています。
●[コード8] w.c - main()
350 for (ep = ehead; ep != NULL; ep = ep->next) {
351 char host_buf[UT_HOSTSIZE + 1];
352 struct sockaddr_storage ss;
353 struct sockaddr *sa = (struct sockaddr *)&ss;
354 struct sockaddr_in *lsin = (struct sockaddr_in *)&ss;
355 struct sockaddr_in6 *lsin6 = (struct sockaddr_in6 *)&ss;
356 time_t t;
357 int isaddr;
358
359 host_buf[UT_HOSTSIZE] = '\0';
360 strncpy(host_buf, ep->utmp.ut_host, UT_HOSTSIZE);
361 p = *host_buf ? host_buf : "-";
362 if ((x_suffix = strrchr(p, ':')) != NULL) {
363 if ((dot = strchr(x_suffix, '.')) != NULL &&
364 strchr(dot+1, '.') == NULL)
365 *x_suffix++ = '\0';
366 else
367 x_suffix = NULL;
368 }
369 if (!nflag) {
370 /* Attempt to change an IP address into a name */
371 isaddr = 0;
372 memset(&ss, '\0', sizeof(ss));
373 if (inet_pton(AF_INET6, p, &lsin6->sin6_addr) == 1) {
374 lsin6->sin6_len = sizeof(*lsin6);
375 lsin6->sin6_family = AF_INET6;
376 isaddr = 1;
377 } else if (inet_pton(AF_INET, p, &lsin->sin_addr) == 1) {
378 lsin->sin_len = sizeof(*lsin);
379 lsin->sin_family = AF_INET;
380 isaddr = 1;
381 }
382 if (isaddr && realhostname_sa(fn, sizeof(fn), sa,
383 sa->sa_len) == HOSTNAME_FOUND)
384 p = fn;
385 }
386 if (x_suffix) {
387 (void)snprintf(buf, sizeof(buf), "%s:%s", p, x_suffix);
388 p = buf;
389 }
390 if (dflag) {
391 for (dkp = ep->dkp; dkp != NULL; dkp = debugproc(dkp)) {
392 const char *ptr;
393
394 ptr = fmt_argv(kvm_getargv(kd, dkp, argwidth),
395 dkp->ki_comm, MAXCOMLEN);
396 if (ptr == NULL)
397 ptr = "-";
398 (void)printf("\t\t%-9d %s\n",
399 dkp->ki_pid, ptr);
400 }
401 }
402 (void)printf("%-*.*s %-*.*s %-*.*s ",
403 UT_NAMESIZE, UT_NAMESIZE, ep->utmp.ut_name,
404 UT_LINESIZE, UT_LINESIZE,
405 strncmp(ep->utmp.ut_line, "tty", 3) &&
406 strncmp(ep->utmp.ut_line, "cua", 3) ?
407 ep->utmp.ut_line : ep->utmp.ut_line + 3,
408 W_DISPHOSTSIZE, W_DISPHOSTSIZE, *p ? p : "-");
409 t = _time_to_time32(ep->utmp.ut_time);
410 longattime = pr_attime(&t, &now);
411 longidle = pr_idle(ep->idle);
412 (void)printf("%.*s\n", argwidth - longidle - longattime,
413 ep->args);
414 }
[コード8]は非常に長いループですが、これが最後です。
351行目~389行目では、ユーザーのログイン元ホスト(ep->utmp.ut_host)からホスト名を逆引きし、変数pに代入しています。ホストが逆引きできない場合もしくは-nオプションが指定されている場合は、IPアドレスをそのままpに保持します。
390行目~401行目は-dオプションが指定された場合の処理で、現在その端末からされたすべてのプロセスIDとコマンド文字列を表示するための処理です。
402行目~413行目で現在のログインユーザー情報の表示を行います。ヘッダ項目はすでにw.cの270行目~281行目で出力されています。
まず、402行目~408行目でユーザー名(ep->utmp.ut_name)、端末ID(ep->utmp.ut_lineから先頭のttyを除く)、ログイン元ホスト名(変数p)を出力します。
続いて、409行目~411行目でログイン時間(ep->utmp.ut_time)とアイドル時間(ep->idle)を出力します。ログイン時間とアイドル時間の整形と出力に使用されているpr_attime()、pr_idle()は、w.cと同じディレクトリにあるpr_time.cに定義されています。
最後に412,413行目で現在実行されているコマンド文字列(ep->args)を出力して完了です。
■まとめ
いかがでしたか? たかがwコマンドといえど、今回登場したロードアベレージやプロセス情報の取得で使われているライブラリ関数は、topコマンドやpsコマンドなどいろんなコマンドで使用されているので、覚えておいて損はないと思います。
ロードアベレージ以外にも、sysctl() を使えばシステム情報が意外に簡単に取得できるので、実際にコードを書いて試してみてください。
今回のお話は以上です。またお会いしましょう。
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました