ヤフー株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。LINEヤフー Tech Blog

テクノロジー

ソースコードリーディング(wコマンド編)-ロードアベレージ/ログインユーザ/プロセス情報の取得

こんにちは、みなさんお久しぶりです。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() を使えばシステム情報が意外に簡単に取得できるので、実際にコードを書いて試してみてください。

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

こちらの記事のご感想を聞かせください。

  • 学びがある
  • わかりやすい
  • 新しい視点

ご感想ありがとうございました

このページの先頭へ