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