mini-tmux
Table of Contents
my env
OpenBSDも7.7が出てsysupgradeしたんだけど、そこから取り残されている環境が ある。7.6の世界ね。 qemu+kvmで動作させてる者だ。ええ怠慢ですよ。でも、これってカーネルをgdb できる用に作成したものなんで、アップデートが面倒なんだ。どうせカーネル観察用 にしか使わないので、これでいいかとなった。
ん訳で、これからtmuxの探検を始めるけど、環境は7.6の世界ね。場合によっては カーネル側にも言及するかも知れないのでね。
それから、もう一つの利点として、qemuで動作させてる場合、シリアルラインを容易 に利用できる事だ。但しこの端末だと、vt220と認識されちゃうので(rows 24)、.profileに 仕掛けをしてる。
[ `tty` == "/dev/tty00" ] && stty rows 35
普通はifを使うんだろうけど、ChatGPTご推薦のlisp風な記述をしてみた。肝は test == [ ってのと、論理式を && で結合するって方式ね。久しぶりに思い出させてくれたね。
最初の一歩
tmux -v すると膨大なログを吐いてくれるって言う嬉しい機能が有るんで、有効 利用させて貰おう。これって随所に盛り込んだprint debugの変形だ。作者 さんは、ここ大事な所だから押さえておけ、ってのの発露だと思うんだ。
題材は、前回やった、sim-gdb.shにする。このスクリプトの中で最初に表われるtmuxの 所をtmux -vとすればいいだろう。
sim-gdb.shを起動して、C-t w それから、q q で終了させた。
ad$ wc tmux-* 246 2111 16923 tmux-client-16840.log 30413 164844 1890590 tmux-server-90124.log
サーバー側のログが、とんでもない行数だな。めげずに冒頭部分だけをチラ見する。
ad$ head -4 tmux-client-16840.log 1746652959.621441 client started (16840): version openbsd-7.6, socket /tmp/tmux-1000/default, protocol 8 1746652959.621525 on OpenBSD 7.6 DEEPSEEK#1; libevent 1.4.15-stable (kqueue) 1746652959.621660 flags are 0x18000000 1746652959.621676 socket is /tmp/tmux-1000/default ad$ head -4 tmux-server-90124.log 1746652959.629689 server started (90124): version openbsd-7.6, socket /tmp/tmux-1000/default, protocol 8 1746652959.629724 on OpenBSD 7.6 DEEPSEEK#1; libevent 1.4.15-stable (kqueue) 1746652959.629899 input_key_build: 0x10e003 (PasteStart) is \033[200~ 1746652959.629908 input_key_build: 0x10e004 (PasteEnd) is \033[201~
クライアント側がキックしてサーバーが動き出すとな。プロトコルバージョンって事は 通信の規約が定義されてるんだな。それからlibevent大事だよとな。
クライアント側のログは、こんな風になってた。
1746652959.622651 add peer 0x3f7b8b8a000: 7 (0x0) 1746652959.625157 sending message 111 to peer 0x3f7b8b8a000 (8 bytes) 1746652959.625252 sending message 111 to peer 0x3f7b8b8a000 (8 bytes) 1746652959.625285 sending message 101 to peer 0x3f7b8b8a000 (5 bytes) : 1746652959.628304 sending message 106 to peer 0x3f7b8b8a000 (0 bytes) 1746652959.631174 cmd_pack_argv: argv[0]=new-session 1746652959.631183 cmd_pack_argv: argv[1]=-d 1746652959.631189 cmd_pack_argv: argv[2]=-s 1746652959.631195 cmd_pack_argv: argv[3]=simgdb 1746652959.631201 cmd_pack_argv: argv[4]=-n 1746652959.631207 cmd_pack_argv: argv[5]=ksh 1746652959.631215 sending message 200 to peer 0x3f7b8b8a000 (36 bytes) 1746652959.631223 client loop enter 1746652959.631287 client_signal: Child exited 1746652959.789017 peer 0x3f7b8b8a000 message 203 1746652959.789070 client loop exit
スクリプト中に定義されてたtmuxへの引数が散見されるな。
ソースを改変
上のログを見ると、最初の項は、事象が発生した時刻を正確にログしてる事が分る。 高速トレードなんてやっていないんで、そんな正確に時刻表示されたって煩いだけだ。 秒以下の部分は目ざわりなんで、取り払ってしまえ。client.c あたりを眺めてみると
static int client_get_lock(char *lockfile) { int lockfd; log_debug("lock file is %s", lockfile);
こんなのが出てくるんで、これを手がかりに追跡して、
ad$ diff -u /usr/src/usr.bin/tmux/log.c log.c --- /usr/src/usr.bin/tmux/log.c Sun Sep 1 13:41:52 2024 +++ log.c Thu May 8 05:25:13 2025 @@ -116,8 +116,8 @@ free(s); gettimeofday(&tv, NULL); - if (fprintf(log_file, "%lld.%06d %s%s\n", (long long)tv.tv_sec, - (int)tv.tv_usec, prefix, out) != -1) + if (fprintf(log_file, "%lld %s%s\n", (long long)tv.tv_sec, + prefix, out) != -1) fflush(log_file); free(out); }
こういう改変をした。ええ、元ファイルは温存しといて、ツリーを/tmp/myxにcpして、 そこでの作業ですよ。
ad$ grep log_debug *.c | sed 's/:.*//' | uniq -c | sort -nr | head 72 server-client.c 47 input.c 31 tty.c 29 screen-write.c 26 cmd-find.c 23 window.c 21 tty-keys.c 21 resize.c 17 tty-term.c 17 format.c ad$ grep log_debug *.c | sed 's/:.*//' | uniq -c | sort -nr | wc 51 102 815 ad$ grep log_debug *.c | wc 541 3820 34483
どのファイルに何箇所ぐらいdebug文が埋め込まれているか。ファイルの個数、それに 監視ポイント数も調べてみた。
ad$ grep log_debug *.c | sed 's/:.*//' | uniq -c | grep cmd- 2 cmd-display-menu.c 1 cmd-display-panes.c 26 cmd-find.c 13 cmd-parse.c 3 cmd-pipe-pane.c 12 cmd-queue.c 2 cmd-refresh-client.c 2 cmd-source-file.c 6 cmd-wait-for.c
コマンドを定義したファイルで重要っぽいのいのには、こんなのが列挙されてた。
check command
折角1秒単位のログにしたんで、ゆっくり実験。/tmp/myx/tmux -v で特製tmuxを起動 。暫くして、tmux display "THIS IS /tmp/myx/tmux" を実行。しばし休んだ後。exitで tmuxを終了。時刻毎に、ログの行数を確認。
ad$ cut -d' ' -f1 tmux-client-44099.log | uniq -c 240 1746743131 4 1746743919 ad$ cut -d' ' -f1 tmux-server-98638.log | uniq -c 19460 1746743131 25 1746743132 7 1746743136 281 1746743146 411 1746743161 281 1746743176 281 1746743191 : 281 1746743897 281 1746743912 78 1746743913 78 1746743914 134 1746743915 32 1746743916 270 1746743919
クライアントのログを時刻に翻訳すると、07:25:31に起動して07:38:39に終了 してる事がわかる。それに対してサーバー側では、15秒間隔で動作してる。 これは、ステータス・ラインに表示されてる時計の更新だな。たかが時刻の 更新でも、随分ログが出てるんだな。
ad$ cut -d' ' -f1 tmux-server-98638.log | uniq -c | sort -nr | head 19460 1746743131 720 1746743475 613 1746743476 569 1746743401 422 1746743462 411 1746743882 :
07:31:15頃にステータスラインに特製メッセージを表示してるんだな。そのあたりを 調べると、ちゃんと痕跡が残っていたぞ。
1746743475 cmd_unpack_argv: argv[0]=display 1746743475 cmd_unpack_argv: argv[1]=THIS IS /tmp/myx/tmux 1746743475 cmd_parse_build_commands 0:0: display 1746743475 cmd_parse_build_commands 0:1: THIS IS /tmp/myx/tmux 1746743475 args_parse_flags: next THIS IS /tmp/myx/tmux 1746743475 args_parse: flags end at 1 of 2 1746743475 args_parse: 1 = THIS IS /tmp/myx/tmux (type STRING) 1746743475 cmd_parse_build_commands: display-message "THIS IS /tmp/myx/tmux" 1746743475 cmdq_get_command: [display-message/0x2c0de4c6600] group 2775 1746743475 cmdq_append </dev/ttyp0>: [display-message/0x2c0de4c6600] 1746743475 cmdq_append </dev/ttyp0>: [server_client_command_done/0x2c0de4b9b00]
from ChatGPT
なかなか手強いので、聞いてみた。
tmuxの内部構造を理解するための追跡方法の提案
- コマンド処理の流れを把握するポイント
tmuxのクライアントからの入力(tmux display "HELLO WORLD"など)は、以下のような流れで処理されます:
- クライアント→サーバへコマンド送信
– クライアントは、UNIXドメインソケット(/tmp/tmux-UID/defaultなど)経由でサーバにコマンド列(argv)を送信します。
– ソース: client.cの client_main()
、 client_send_command()
あたり。
- サーバがargvを受信し、コマンドに変換
– cmd_unpack_argv()
でargvが構造体struct cmd_parse_result
に変換される。
– cmd_list_parse()
がキモ。ここでcmdq用のcmd構造体が作られます。
- cmd queueに追加される
– cmdq_append()
などでコマンドが登録される。
– cmdq_continue()
でコマンドが1つずつ実行される。
- 各cmdのexec関数が呼ばれる
– 例: cmd_display_message_exec()
がdisplayの実装。
ここまでの処理がログで1300行にわたって出力されているわけです。多いですが、概ね上記の分岐で整理できます。
- 効果的なログ追跡方法
tmux -vvvv -Ltest -f/dev/null などとすると、余計な設定ファイルも読み込まれず、ログもシンプルになります。
以下のログレベルが特に役立ちます:
- cmd*: コマンドの分解・登録・実行系(最も重要)
- client*, server*: クライアント・サーバ間の処理
- tty*, screen*: 実際の表示系(ウィンドウ描画)
ログがシンプルになるって触れ込みだったけど、より複雑 になったぞ。典型的なハルシネーションだな。
ad$ wc tmux-*.log 244 2127 15179 tmux-client-67835.log 104 135 1743 tmux-out-93729.log 24603 138502 1333299 tmux-server-93729.log
tty/pty
tmuxの要はtty/ptyの技術を使って、仮想端末を増やしている。下記の様に、確かに 増量してるな。
ad$ cc ob-samle.c -lutil ad$ tty /dev/ttyp0 ad$ ./a.out ad$ tty tty /dev/ttyp1 ^C ad$
基本のコード
// tty/pty sample #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <util.h> #include <termios.h> #include <string.h> #include <sys/select.h> int main() { int master_fd; pid_t pid; struct termios term; tcgetattr(STDIN_FILENO, &term); pid = forkpty(&master_fd, NULL, &term, NULL); if (pid == 0) { execlp("ksh", "ksh", NULL); // using ksh perror("execlp"); exit(1); } fd_set fds; char buf[1024]; while (1) { FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); FD_SET(master_fd, &fds); int maxfd = (STDIN_FILENO > master_fd) ? STDIN_FILENO : master_fd; if (select(maxfd + 1, &fds, NULL, NULL, NULL) > 0) { if (FD_ISSET(STDIN_FILENO, &fds)) { ssize_t len = read(STDIN_FILENO, buf, sizeof(buf)); if (len > 0) write(master_fd, buf, len); } if (FD_ISSET(master_fd, &fds)) { ssize_t len = read(master_fd, buf, sizeof(buf)); if (len > 0) write(STDOUT_FILENO, buf, len); } } } return 0; }
例によってgdb祭
(gdb) bt #0 forkpty (amaster=<optimized out>, name=<optimized out>, termp=<optimized out>, winp=<optimized out>) at /usr/src/lib/libutil/pty.c:102 #1 0x0000035430f51bb5 in main () at ob-samle.c:18
そして、すかさずman
The forkpty() function combines openpty(), fork(), and login_tty() to create a new process operating in a pseudo-tty. The file descriptor of the master side of the pseudo-tty is returned in amaster, and the filename of the slave in name if it is non-null. The termp and winp parameters, if non-null, will determine the terminal attributes and window size of the slave side of the pseudo-tty.
今度はtmuxにガサ入れします。
ad$ grep forkpty *.c job.c: pid = fdforkpty(ptm_fd, &master, tty, NULL, &ws); spawn.c: new_wp->pid = fdforkpty(ptm_fd, &new_wp->fd, new_wp->tty, NULL, &ws);
親戚でした。
The fdopenpty() and fdforkpty() functions work like openpty() and forkpty() but expect a /dev/ptm file descriptor ptmfd obtained from the getptmfd() function.
mini-tmux
今度は本命のmini-tmux原理の実装。C-b 1,2,3 で3画面を切り替え。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <termios.h> #include <util.h> #include <string.h> #include <sys/select.h> #include <signal.h> #define NUM_PTY 3 int master_fd[NUM_PTY]; pid_t pids[NUM_PTY]; int current = 0; void spawn_shell(int index) { struct termios term; tcgetattr(STDIN_FILENO, &term); pids[index] = forkpty(&master_fd[index], NULL, &term, NULL); if (pids[index] == 0) { execlp("ksh", "ksh", NULL); perror("execlp"); exit(1); } } int main() { for (int i = 0; i < NUM_PTY; i++) { spawn_shell(i); } fd_set fds; char buf[256]; // canonical(raw) mode struct termios orig, raw; tcgetattr(STDIN_FILENO, &orig); raw = orig; cfmakeraw(&raw); tcsetattr(STDIN_FILENO, TCSANOW, &raw); int switch_mode = 0; while (1) { FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); FD_SET(master_fd[current], &fds); int maxfd = (STDIN_FILENO > master_fd[current]) ? STDIN_FILENO : master_fd[current]; if (select(maxfd + 1, &fds, NULL, NULL, NULL) > 0) { if (FD_ISSET(STDIN_FILENO, &fds)) { ssize_t len = read(STDIN_FILENO, buf, sizeof(buf)); if (len > 0) { if (switch_mode) { if (buf[0] >= '1' && buf[0] < '1' + NUM_PTY) { current = buf[0] - '1'; write(STDOUT_FILENO, "\033[2J\033[H", 7); dprintf(STDOUT_FILENO, "[switched to pty %d]\n", current + 1); } switch_mode = 0; continue; } if (buf[0] == '\x02') { // Ctrl-b switch_mode = 1; continue; } write(master_fd[current], buf, len); } } if (FD_ISSET(master_fd[current], &fds)) { ssize_t len = read(master_fd[current], buf, sizeof(buf)); if (len > 0) write(STDOUT_FILENO, buf, len); } } } tcsetattr(STDIN_FILENO, TCSANOW, &orig); // normal(cooked) mode return 0; }
tmuxやviのようなアプリケーションはrawモード+状態遷移でコマンド入力を処理
する必要が有る。その為、状態フラグ switch_mode
を導入して処理をしている。
本物のtmuxは、さぞや大変な事をやってるのだろな。
pstreeで関連部分を確認
| \-+= 11058 sakae ./a.out | |--= 22495 sakae ksh | |--= 83413 sakae ksh | \--= 06001 sakae ksh
こんな風に起動したものだ。確かにそれぞれの端末でkshが動作してる。
ad$ ./a.out ad$ echo $$ 22495 ad$ tty /dev/ttyp2 [switched to pty 2] ;; C-b 2 ad$ ad$ echo $$ 83413 ad$ tty /dev/ttyp3
README
オール電化・雨月物語 なんて本を読んだ。帯には、本当に怖いのは、人間の業と 電化製品。雨月物語近未来の新感覚ホラーとあった。
オール電化でホラーと言われたら、真っ先に思い出すのは、地震とか水害でタワマンに 通電出来ない事態。49階までどうやって登るねん。水の出ないトイレはどうすんねん。
まだまだ有るぞ。先月だったな、高速のETCがおかしくなって、高速道路から出るのに 4989してたのも思い出される。強欲な道路会社は、ユーザーの迷惑より自分達の利益 を優先させた。電子決済が停止しちゃって、買い物もできなくなった等等。 オイラーは何時もニコニコ現金払いさ。
それにしてもオイラーは 雨月物語 を、知らなかったぞ。