ps

Table of Contents

no buffering

Q: C言語で利用されるprintfについて質問します。あるアプリはprintfを利用して出力しています。このアプリの出力をリダイレクトして、アプリの外側でtail -f してモニターしようとしています。 しかしアプリが終了するまで、tail -f からの出力はありません。 多分、バッファリングされている為と推測しています。 環境変数を変更するなどして、バッファリングを無効にする方法はありますか?

A: はい、`printf` の出力がバッファリングされることが原因で `tail -f` にすぐに出力が反映されない可能性があります。これは、標準出力がデフォルトで行バッファリングされているためです。この問題を解決する方法として、いくつかの方法があります。

### 方法1: `setvbuf` を使用してバッファリングを無効にする `setvbuf` 関数を使用して、標準出力のバッファリングを無効にすることができます。これにより、出力がすぐに表示されます。以下はその例です。

#include <stdio.h>

int main() {
    setvbuf(stdout, NULL, _IONBF, 0); // バッファリングを無効にする
    printf("Hello, World!\n");
    // 他のコード
    return 0;
}

### 方法2: 標準エラー出力(stderr)を使用する 標準エラー出力(`stderr`)はデフォルトでバッファリングされないため、これを利用する方法もあります。

#include <stdio.h>

int main() {
    fprintf(stderr, "Hello, World!\n");
    // 他のコード
    return 0;
}

### 方法3: `fflush` を使用してバッファを手動でフラッシュする 各 `printf` の後に `fflush(stdout)` を呼び出すことで、バッファを手動でフラッシュすることができます。

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    fflush(stdout); // バッファをフラッシュ
    // 他のコード
    return 0;
}

### 方法4: 環境変数 `stdbuf` を使用する シェルで `stdbuf` コマンドを使用して、プログラムの実行時に標準出力のバッファリングを制御できます。

stdbuf -o0 ./your_program | tail -f

`-oL` は行バッファリングを有効にし、`-o0` は完全にバッファリングを無効にします。

以上の方法を使用することで、`printf` の出力を即座に反映させることがで きます。どの方法を選ぶかは、具体的な要件や環境によりますが、プログラム コードに変更を加えることができる場合は、最初の方法(`setvbuf`)や、 `fflush` を使用する方法が一般的です。

ps

ob$ ps -Ao pid,command | grep :
30739 slaacd: engine (slaacd)
39037 slaacd: frontend (slaacd)
89247 smtpd: crypto (smtpd)
83692 smtpd: control (smtpd)
19692 smtpd: lookup (smtpd)
92533 smtpd: dispatcher (smtpd)
86592 smtpd: queue (smtpd)
80202 smtpd: scheduler (smtpd)
 5915 tmux: server (/tmp/tmux-1000/default) (tmux)
39602 tmux: client (/tmp/tmux-1000/default) (tmux)
75632 sshd: /usr/sbin/sshd -p 2022 [listener] 0 of 10-100 startups (sshd)

psを実行するとcommandの所に、注釈みたいのが出てくる。この仕組みって、 どう実現されているのだろう? 注釈の元になるデータは、psがカーネルから 取り寄せた物と想像できる。少し探ってみるかな。

watch output

gdbを使う時、layout src してソースを眺めながらやるのがオイラーの流行な んだけど、ターゲットのpsの出力のせいか、頻繁に画面のレイアウトが崩れる。 ならば、emacsからgdbするかと昔の方法に戻る事にする。

でも、psの出力がemacsの画面に割り込みしてきて鬱陶しい。emacsをgdbのコ ンパネの様に使用する方法もあるけど、窓が多すぎてオイラーの趣味に合わな い。で、思いついたのが、psの出力をリダイレクトしちゃえ。そして、別端末 でtail -f すればいいじゃんと言う、unixの原点的な解決方法。

アイデアはいいと思うんだけど、実際にやってみると、出力が観測されなかっ た。ターゲットのpsが終了すると、バーとps出力が出てくる。ははは、あのせ いだなとなって、冒頭の質問をしたのさ。

ソースが有るので、setvbuf(stdout, NULL, _IONBF, 0); を、適当な所にぶち 込んでpsをコンパイルしたよ。

ps.c

        /*
         * for each proc, call each variable output function.
         */
        setvbuf(stdout, NULL, _IONBF, 0); // <------ add this line
        for (i = lineno = 0; i < nentries; i++) {
                if (xflg == 0 && ((int)pinfo[i].ki->p_tdev == NODEV ||
                    (pinfo[i].ki->p_psflags & PS_CONTROLT ) == 0))
                        continue;
                if (showthreads && pinfo[i].ki->p_tid == -1)
                        continue;
                for (vent = vhead; vent; vent = vent->next) {
=>                      (vent->var->oproc)(&pinfo[i], vent);

こんな感じでリダイレクトする。

(gdb) b command
Breakpoint 1 at 0x4d7d: file print.c, line 113.
(gdb) r -Ao pid,command > /tmp/LOG
Starting program: /home/sakae/ps/ps -Ao pid,command > /tmp/LOG

Breakpoint 1, command (pi=0x6c094c80, ve=0x6c06b440) at print.c:113
113             const struct kinfo_proc *kp = pi->ki;
(gdb) bt
#0  command (pi=0x6c094c80, ve=0x6c06b440) at print.c:113
#1  0x1a76104e in main (argc=0, argv=0xcf7c9950) at ps.c:393

in print.c/command()

                                argv = kvm_getargv(kd, kp, termwidth);
                                if ((p = argv) != NULL) {
                                        while (*p) {
                                                if (wantspace) {
                                                        putchar(' ');
                                                        left--;
                                                }
=>                                              left -= mbswprint(*p, left, 0);
                                                if (left == 0)

その時の値

(gdb) p *p
$2 = 0x696f7008 "smtpd: lookup"

次に進めると、psの出力は、こうなった。

ob$ tail -f /tmp/LOG
  PID COMMAND
   :
89247 smtpd: crypto <smtpd>
83692 smtpd: control <smtpd>
19692 smtpd: lookup

example of setproctitle

今度はアプリ側がどうなっているか、調べてみる。まずは、特徴的なメッセー ジを提示している、sshdからだ。

sshd

こういうのは、grepの仕事だな。探してみると、 sshd.c:1119 で見つかった。

if (ostartups != startups) {
        setproctitle("%s [listener] %d of %d-%d startups",
            listener_proctitle, startups,
            options.max_startups_begin, options.max_startups);
        ostartups = startups;

gdbしてみる。

(gdb) p startups
$1 = 0
(gdb) p options.max_startups_begin
$2 = 10
(gdb) p options.max_startups
$3 = 100
(gdb) p listener_proctitle
$4 = 0x3ddfaa80 "/home/sakae/sshd -p2022 -D"

まだ、ちょっと、sshd: … が、足りない。こういう時は、setproctitle()で、 何かやってないか潜れだな。が、この関数は、sshdのソース群では定義されて いなかった。ワールドワイドな関数になっていなか、manする。

DESCRIPTION
     The setproctitle() function sets the invoking process's title.  The
     process title is set to the last component of the program name, followed
     by a colon, a single space, and the formatted string specified by fmt.
     If fmt is NULL, the colon and formatted string are omitted.  The length
     of a process title is limited to 2048 bytes.

したら、公共な関数になってた。潜って行って、出口では、こんな獲物を掴ま える事が出来たぞ。海に行かなくても、獲物をキャッチできるんです。

(gdb) p bufp
$8 = 0x2a5af020 <setproctitle.buf> "sshd: /home/sakae/sshd -p2022 -D [listener] 0 of 10-100 startups"

tmux

今度は、日頃お世話になってる、tmuxに狙いを定めてみます。これも特徴的な 姿をしているんだよな。

ob$ ps awx | grep tmux
64604 ??  Rp       0:00.53 tmux: server (/tmp/tmux-1000/default) (tmux)
57749 p0  I+p      0:00.01 tmux: client (/tmp/tmux-1000/default) (tmux)
ob$ grep setproctitle *.c
proc.c: setproctitle("%s (%s)", name, socket_path);

一発で、仕留められたけど、学術的な興味があります。

struct tmuxproc *
proc_start(const char *name)
{
        struct tmuxproc *tp;
        struct utsname   u;

        log_open(name);
        setproctitle("%s (%s)", name, socket_path);

        if (uname(&u) < 0)
                memset(&u, 0, sizeof u);

        log_debug("%s started (%ld): version %s, socket %s, protocol %d", name,
            (long)getpid(), getversion(), socket_path, PROTOCOL_VERSION);
        log_debug("on %s %s %s; libevent %s (%s)", u.sysname, u.release,
            u.version, event_get_version(), event_get_method());

近辺に、ログも取得できると説明がありました。manと連携させると、-v -vv とかが肝みたい。

at current dir tmux-client-56440.log

1722550069.701113 client started (56440): version openbsd-7.5, socket /tmp/tmux-1000/default, protocol 8
1722550069.701192 on OpenBSD 7.5 GENERIC#175; libevent 1.4.15-stable (kqueue)
1722550069.701337 flags are 0x18000000
1722550069.701380 socket is /tmp/tmux-1000/default
1722550069.701408 trying connect
 :

more

もっと、もっとと欲張りしてみます。宝庫っぽい、/usr/src/usr.sbin

ob$ grep setproctitle -r .
./cron/atrun.c: setproctitle("atrun %s", atfile);
./cron/do_command.c:    setproctitle("running job");
./ntpd/ntp.c:   setproctitle("ntp engine");
./ntpd/ntp_dns.c:       setproctitle("dns engine");
 :

/usr/src/sbinでも、多少見掛けるなあ。

MyProg

setproctitleの挙動を知りたいので、下記の実験コードを作成。2回コールし たら、それが反映されるかに興味が有ったので。

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

#define SEC 10

int main(int argc, char *argv[]){
  setproctitle("foo (%d)", getpid());
  sleep(SEC);
  printf("past %dsec\n", SEC);
  setproctitle("MyProg pid is (%d)", getpid());
  sleep(SEC);
}

SECの待ち時間を設けているのは、psで確認する余裕を得るためだ。起動した ら直ぐにps。時間が経過したよと言われたら、もう一度psを実行。

ob$ ps
  PID TT  STAT        TIME COMMAND
64612 p2  S+       0:00.00 a.out: foo (64612) (a.out)
ob$ ps
  PID TT  STAT        TIME COMMAND
64612 p2  S+       0:00.00 a.out: MyProg pid is (64612) (a.out)

ちゃんと追従してるね。

原理追求

どういう仕掛けで、これを実現してるのだろう? 先の調べは簡単すぎて、原 理まで踏み入っていなかった。 詳細に点検すべきは、 /usr/src/lib/libc/gen/setproctitle.c

この前半は、 snprintf(buf, MAX_PROCTITLE, "%s: ", __progname); から 始まるprintf相当 だ。但し、bufは、ずっと居座るタイプとして、宣言されてる。後半は、

void
setproctitle(const char *fmt, ...) {
        static struct ps_strings *ps;
          :
        if (ps == NULL) {
                struct _ps_strings _ps;
                const int mib[2] = { CTL_VM, VM_PSSTRINGS };
                size_t len;

                len = sizeof(_ps);
                if (sysctl(mib, 2, &_ps, &len, NULL, 0) != 0)
                        return;
                ps = (struct ps_strings *)_ps.val;
        }
=>      ps->ps_nargvstr = 1;
        ps->ps_argvstr = &bufp;

初回は、psがNULLなので、if内が実行される。

(gdb) p *ps
$4 = {
  ps_argvstr = 0xcf7d02b4,
  ps_nargvstr = 1,
  ps_envstr = 0xcf7d02bc,
  ps_nenvstr = 20
}

その結果、カーネル側のps構造体の一部が得られる。メンバー名から、プロセ スの起動に関連するデータと推測される。

(gdb) p *(ps->ps_argvstr)
$5 = 0xcf7d0358 "/home/sakae/a.out"
(gdb) p bufp
$6 = 0x24443020 <setproctitle.buf> "a.out: foo (27012)"

最初のsetproctitleが呼ばれた時の状態。

(gdb) p *(ps->ps_argvstr)
$9 = 0x24443020 <setproctitle.buf> "a.out: MyProg pid is (27012)"
(gdb) p bufp
$10 = 0x24443020 <setproctitle.buf> "a.out: MyProg pid is (27012)"

2回目のコール時の結果。初回の最後で、 ps->ps_argvstr = &bufp; が実行 されているので、bufと共有みたいな状態になってるな。

at FreeBSD

FreeBSDの場合、12.0から、こんなコードが提供されてる。OpenBSDとほぼ同義 だな。

static int fast_update = 0;

void
setproctitle_fast(const char *fmt, ...)
{
        va_list ap;
        char *buf;
        int oid[4];

        va_start(ap, fmt);
        buf = setproctitle_internal(fmt, ap);
        va_end(ap);

        if (buf && !fast_update) {
                /* Tell the kernel to start looking in user-space */
                oid[0] = CTL_KERN;
                oid[1] = KERN_PROC;
                oid[2] = KERN_PROC_ARGS;
                oid[3] = -1;
                sysctl(oid, 4, 0, 0, "", 0);
                fast_update = 1;
        }
}

setproctitle_internal ってのをわざわざ自前で(注意を払って)実装して ますねぇ。その分、複雑になってる。普通のsetproctitleの核心は、

if (buf != NULL) {
        /* Set the title into the kernel cached command line */
        oid[0] = CTL_KERN;
        oid[1] = KERN_PROC;
        oid[2] = KERN_PROC_ARGS;
        oid[3] = -1;
        sysctl(oid, 4, 0, 0, buf, strlen(buf) + 1);
        fast_update = 0;
}

だからねぇ。OpenBSDの様な割り切りが好きだな。ああ、割り切りといえば、 皆がこぞって利用してるOPENSSLだけど、OpenBSDでは、BUGに辟易してそこか らフォークしてる。その時、余り利用されていない機能をばっさり廃止。それ に伴なって9万行を削除したとか。BUGの数はソースの行数と相関が有るだろう から、良い決断だったと思うよ。sudoもしかりで、不要(と思われる)機能を削 除した、doasに置き換えているしね。

at ArchLinux

リナ方面はどうなっているか?

[sakae@arch MY]$ ps -Ao pid,command|grep :
     :
    371 dhcpcd: [manager] [ip4] [ip6]
    372 sshd: /usr/bin/sshd -D [listener] 0 of 10-100 startups
     :

勿論、setproctitleなんて関数は無い。だって、NetBSD由来ですから。 その代わり、何でも /procって事で、カーネルの事情は許す限り、ここに公開 されている。

[sakae@arch ~]$ cat /proc/372/cmdline | tr '\000' '\n'
sshd: /usr/bin/sshd -D [listener] 0 of 10-100 startups

だから、こんなコマンドを叩くだけで、手軽に入手できるぞ。 でも、これを実現する為には、sshdのソースを、それなりに改変する必要が有 るだろうね。どんな改変をしてるのだろう?

etc