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のソースを、それなりに改変する必要が有 るだろうね。どんな改変をしてるのだろう?