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してたのも思い出される。強欲な道路会社は、ユーザーの迷惑より自分達の利益 を優先させた。電子決済が停止しちゃって、買い物もできなくなった等等。 オイラーは何時もニコニコ現金払いさ。
それにしてもオイラーは 雨月物語 を、知らなかったぞ。