mount namespaceむずすぎるっピ〜になった
clone(CLONE_NEWNS)で新しいmount namespaceでbashとか上げられますね…そこから親も子も同じ/tmpの中身が見えますね…まではいい
ここで子の中でmount -t tmpfs tmpfs /tmpするとどうなるか. 子からは新しく空っぽのtmpfsが見える. 親の方は? 新しいnamespaceなんだから, 親に影響出ないでほしい…が, /tmpはpropagaton=sharedなので子でのmount /tmpが親にも伝わり親の方も空っぽ/tmpになっちゃう.
これをしないためには, 子の方でmount --make-private /tmpして, propagationを切ってやるといい. となった時に, アレこれって親の/tmpのpropagtionも変わる?となるけど変わらない. 結局ここは別のnamespaceなので.
基本, 別のnamespaceなのでmountは互いに無関係なのだけど, propagationによって相手に伝わる…し, manの例は/tmp/fooに新しくmountしたものが相手にも見えますね〜としてて, /tmp自体もpropagationがあることがよくわかってなかった
XFSの2038年問題にオンラインでも勝利したい
tldr;
これの詳しい話. また, オンラインでも変換できることを示す.
検証結果: XFS全体フラグのbigtimeを立てても, 古いinode内のフラグはbigtime=0でタイムスタンプのバイナリもそのまま.
— イーロン・マスクツイッターやめろ (@naota344) 2024年10月16日
新しいinodeはbigtime=1で新しいフォーマットになる. 次に古いinodeをtouchしたらbigtime=1になって, フォーマットも変わる. https://t.co/QPSsKcfisD
XFSのbigtimeとは?
XFSのタイムスタンプは現状では, signed 32bitがUNIXエポックタイム(1970年1月1日 00:00:00 UTC)からの秒数で, 32bitがナノ秒部分となっています. これだと2038年以後の日時を表現できなくなります. そこで, その2つのフィールドをまとめて64bitにして, signed 32bitで表現できていた最小のタイムスタンプ(1901年12月13日 20:45:52 UTC, signedなのでマイナスも表現できてエポックより前のここが最小)からの経過ナノ秒でタイムスタンプを表現することにします. これがbigtimeという機能で, 2486年7月頃までの表現が可能になります.
ようするに, これが
/* Legacy timestamp encoding format. */ struct xfs_legacy_timestamp { __be32 t_sec; /* timestamp seconds */ __be32 t_nsec; /* timestamp nanoseconds */ };
これになる
typedef __be64 xfs_timestamp_t;
bigtimeの変換を見てみよう
おおよそ10年以内, 2013年以後に作られたXFSであれば, オフラインで(mountしていない状態で)bigtimeを有効にできます. 実験してみましょう.
まずは, 適当なところにXFSのファイルシステムイメージを作りmountします. もうデフォルトでbigtimeが有効な場合もあるので, "-m bigtime=0"でbigtimeを明示的に無効化しておきます.
$ rm xfs2.img
$ truncate -s 10G xfs2.img
$ mkfs.xfs -m bigtime=0 xfs2.img
meta-data=xfs2.img isize=512 agcount=4, agsize=655360 blks
= sectsz=512 attr=2, projid32bit=1
= crc=1 finobt=1, sparse=1, rmapbt=1
= reflink=1 bigtime=0 inobtcount=1 nrext64=1
= exchange=0
data = bsize=4096 blocks=2621440, imaxpct=25
= sunit=0 swidth=0 blks
naming =version 2 bsize=4096 ascii-ci=0, ftype=1, parent=0
log =internal log bsize=4096 blocks=16384, version=2
= sectsz=512 sunit=0 blks, lazy-count=1
realtime =none extsz=4096 blocks=0, rtextents=0
$ sudo mount xfs2.img /mnt/scratch
"old"というファイルに適当なタイムスタンプをつけて, umountします.
$ sudo touch -t 202410160900 /mnt/scratch/old $ sudo umount /mnt/scratch
xfs_adminでbigtimeを有効にします. さくっと終わります.
$ xfs_admin -O bigtime=1 xfs2.img Running xfs_repair to upgrade filesystem. ... Phase 1 - find and verify superblock... ... Phase 7 - verify and correct link counts... done
新しいファイル"new"に同じタイムスタンプをつけて, umountします.
$ sudo mount xfs2.img /mnt/scratch $ sudo touch -t 202410160900 /mnt/scratch/new $ sudo umount /mnt/scratch
ここからxfs_dbを使ってinodeをチェックします. まず"info"でスーパーブロックの情報を見ると, "bigtime=1"とフラグがついていることがわかります.
xfs_db> info
meta-data=xfs.img isize=512 agcount=4, agsize=655360 blks
= sectsz=512 attr=2, projid32bit=1
= crc=1 finobt=1, sparse=1, rmapbt=1
= reflink=1 bigtime=1 inobtcount=1 nrext64=1
= exchange=0
data = bsize=4096 blocks=2621440, imaxpct=25
= sunit=0 swidth=0 blks
naming =version 2 bsize=4096 ascii-ci=0, ftype=1, parent=0
log =internal log bsize=4096 blocks=16384, version=2
= sectsz=512 sunit=0 blks, lazy-count=1
realtime =none extsz=4096 blocks=0, rtextents=0
次に2つのファイルのinodeの中の情報を見ます. まずは"ls /"で2つのファイルのinode番号を確認. 131と132です.
$ xfs_db xfs2.img xfs_db> ls / /: 8 128 directory 0x0000002e 1 . (good) 10 128 directory 0x0000172e 2 .. (good) 12 131 regular 0x001bf664 3 old (good) 14 132 regular 0x001bb2f7 3 new (good)
inode番号131であるファイル"old"を見ます. "v3.bigtime = 0"とinodeの内部にもフラグがあります.
xfs_db> inode 131 xfs_db> p ... core.atime.sec = Wed Oct 16 09:00:00 2024 core.atime.nsec = 0 core.mtime.sec = Wed Oct 16 09:00:00 2024 core.mtime.nsec = 0 core.ctime.sec = Wed Oct 16 09:21:57 2024 core.ctime.nsec = 153635926 ... v3.bigtime = 0 ...
一方, inode番号132であるファイル"new"を見ます. 今度は"v3.bigtime = 1"です.
xfs_db> inode 132 xfs_db> p ... core.atime.sec = Wed Oct 16 09:00:00 2024 core.atime.nsec = 0 core.mtime.sec = Wed Oct 16 09:00:00 2024 core.mtime.nsec = 0 core.ctime.sec = Wed Oct 16 09:23:32 2024 core.ctime.nsec = 281349756 ... v3.bigtime = 1 ...
すなわち, スーパーブロックとそれぞれのinodeの両方にbigtimeのフラグがあります. それぞれのinodeのbigtimeフラグは, そのinode内のタイムスタンプが新旧どちらの表現であるかを示します. スーパーブロックのフラグが立っていれば, 新しいファイルやファイルの書きかえ時に新しいフォーマットのタイムスタンプに書き変わります.
ダンプを見よう
バイナリダンプを見とかないと終われないので見ましょう.
ファイル"old"はinode番号が131で, inodeのサイズが512バイト(infoのisizeに書いてる)なので, これで見れます.
先頭の"IN"でちゃんとinodeをdumpできていることが確認できます. 周りをあけている, 0x10620からの2行がタイムスタンプ(atime, mtime, ctime)です. dateコマンドでチェックしたところ(big-endianに注意), ちゃんと設定したタイムスタンプが入っています.
$ hexdump -C -s $((131 * 512)) -n 512 xfs2.img 00010600 49 4e 81 a4 03 02 00 00 00 00 00 00 00 00 00 00 |IN..............| 00010610 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010620 67 0f 02 00 00 00 00 00 67 0f 02 00 00 00 00 00 |g.......g.......| 00010630 67 0f 07 25 09 28 4c 56 00 00 00 00 00 00 00 00 |g..%.(LV........| 00010640 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010650 00 00 18 01 00 00 00 00 00 00 00 00 56 5c 1c 43 |............V\.C| 00010660 ff ff ff ff f4 58 94 32 00 00 00 00 00 00 00 04 |.....X.2........| 00010670 00 00 00 01 00 00 00 04 00 00 00 00 00 00 00 10 |................| 00010680 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010690 67 0f 07 25 09 28 4c 56 00 00 00 00 00 00 00 83 |g..%.(LV........| 000106a0 2c 82 1d 4d 14 58 4a 73 8c dd 20 29 7f c7 88 dd |,..M.XJs.. )....| 000106b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00010770 00 2b 01 00 07 1d 04 73 65 6c 69 6e 75 78 73 74 |.+.....selinuxst| 00010780 61 66 66 5f 75 3a 6f 62 6a 65 63 74 5f 72 3a 75 |aff_u:object_r:u| 00010790 6e 6c 61 62 65 6c 65 64 5f 74 00 00 00 00 00 00 |nlabeled_t......| 000107a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00010800 $ date --date=@$((0x670f0200)) Wed Oct 16 09:00:00 AM JST 2024
一方, ファイル"new"の方はこうです. 同様にタイムスタンプの周囲をあけています.
$ hexdump -C -s $((132 * 512)) -n 512 xfs2.img 00010800 49 4e 81 a4 03 02 00 00 00 00 00 00 00 00 00 00 |IN..............| 00010810 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010820 35 cc 2a cf 0b 94 00 00 35 cc 2a cf 0b 94 00 00 |5.*.....5.*.....| 00010830 35 cc 2c 17 de 1b 36 7c 00 00 00 00 00 00 00 00 |5.,...6|........| 00010840 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010850 00 00 18 01 00 00 00 00 00 00 00 00 36 2f 08 59 |............6/.Y| 00010860 ff ff ff ff 38 e7 18 5c 00 00 00 00 00 00 00 04 |....8..\........| 00010870 00 00 00 01 00 00 00 0f 00 00 00 00 00 00 00 18 |................| 00010880 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010890 35 cc 2c 17 de 1b 36 7c 00 00 00 00 00 00 00 84 |5.,...6|........| 000108a0 2c 82 1d 4d 14 58 4a 73 8c dd 20 29 7f c7 88 dd |,..M.XJs.. )....| 000108b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00010970 00 2b 01 00 07 1d 04 73 65 6c 69 6e 75 78 73 74 |.+.....selinuxst| 00010980 61 66 66 5f 75 3a 6f 62 6a 65 63 74 5f 72 3a 75 |aff_u:object_r:u| 00010990 6e 6c 61 62 65 6c 65 64 5f 74 00 00 00 00 00 00 |nlabeled_t......| 000109a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00010a00
タイムスタンプはナノ秒なので…とdateコマンドにスケールだけ変えてつっこむと未来が見えてしまいます. 最初に書いたようにこれは, 昔のタイムスタンプの最小値からのナノ秒です. その値は…"-2147483648" …ってイコール 2の31乗ですね. signed 32bitの最小なのでそれはそう. そのぶん補正すると, ちゃんと同じタイムスタンプがとれます.
$ date --date=@$((0x35cc2acf0b940000/1000000000)) Mon Nov 3 12:14:08 PM JST 2092 $ date --date='Dec 13 20:45:52 UTC 1901' +%s -2147483648 $ date --date=@$((0x35cc2acf0b940000/1000000000 - 2**31)) Wed Oct 16 09:00:00 AM JST 2024
ここでもう一度"old"を同じタイムスタンプでtouchして, umountして, dumpをとります. するとすると, ちゃんと古いフォーマットからbigtimeのフォーマットに変換されているのが見てとれます.
$ hexdump -C -s $((131 * 512)) -n $((512 * 1)) xfs2.img 00010600 49 4e 81 a4 03 02 00 00 00 00 00 00 00 00 00 00 |IN..............| 00010610 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010620 35 cc 2a cf 0b 94 00 00 35 cc 2a cf 0b 94 00 00 |5.*.....5.*.....| 00010630 35 cc 2d fa 8d a4 e9 c7 00 00 00 00 00 00 00 00 |5.-.............| 00010640 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010650 00 00 18 01 00 00 00 00 00 00 00 00 56 5c 1c 43 |............V\.C| ...
むりやり online convertしよう!
さて上記まで見たところ, bigtimeを有効にしても既存のファイルのタイムスタンプはただちに変換されていないことがわかります. 各inodeにフラグがあるので, 次の更新までは元のフォーマットのまま放置していてもかまいせん. xfs_adminがやっていることは結局スーパーブロックのbitを1つ立てているにすぎません.
そんなたかだか1bitのために, umountするなんて嫌じゃないですか? bit1つぐらい実行時で立ててもいいじゃん. 理論上できるでしょ. 立ててやりましょう. kernel moduleで.
!!! 以下のコードは絶対に実行しないようにしましょう. ファイルシステムやシステムが破壊される可能性があります. 責任は一切とれません !!!
お使いのエディタから以下のコードを書きました. 今日はnvimにしました.
"/mnt/tmp"にmountされているXFSを確認し, そのfeatureフラグを立てます. そのフラグはメモリ上にしか影響しないので, diskに書かれるsuperblockのフラグも立てて, 後でxfs_repairに怒られないようにします. ただそれだけのkernel moduleです.
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include <linux/module.h> #include <linux/nsproxy.h> #include <linux/namei.h> #if 0 #include "xfs/xfs.h" #include "xfs/libxfs/xfs_types.h" #include "xfs/libxfs/xfs_format.h" #include "xfs/libxfs/xfs_shared.h" #include "xfs/libxfs/xfs_trans_resv.h" #include "xfs/xfs_mount.h" #endif static int init_xfs_conv(void) { return 0; const char *MNT_DIR = "/mnt/tmp"; pr_info("xfs online convert start"); struct path path __free(path_put) = {}; int ret; ret = kern_path(MNT_DIR, LOOKUP_FOLLOW, &path); if (ret) { pr_err("failed to open %s %d", MNT_DIR, ret); return ret; } struct super_block *sb = path.mnt->mnt_sb; pr_info("xfs sb %px", sb); struct xfs_mount *mp = XFS_M(sb); pr_info("xfs magic %x", mp->m_sb.sb_magicnum); if (mp->m_sb.sb_magicnum != XFS_SB_MAGIC) { pr_err("invalid xfs magic"); return -EINVAL; } uint64_t features = READ_ONCE(mp->m_features); pr_info("xfs feature %llx", features); if (features & XFS_FEAT_BIGTIME) { pr_info("already has XFS_FEAT_BIGTIME"); return 0; } return 0; features |= XFS_FEAT_BIGTIME; WRITE_ONCE(mp->m_features, features); uint64_t sb_feautres = READ_ONCE(mp->m_sb.sb_features_incompat); sb_feautres |= XFS_SB_FEAT_INCOMPAT_BIGTIME; WRITE_ONCE(mp->m_sb.sb_features_incompat, sb_feautres); pr_info("BIGTIME enabled"); return 0; } static void exit_xfs_conv(void) { pr_info("xfs online convert finish"); } module_init(init_xfs_conv); module_exit(exit_xfs_conv); MODULE_DESCRIPTION("DO NOT USE!!! Online Convert XFS!"); MODULE_LICENSE("GPL");
同様に, XFSをmountして"old"をtouchします. さっきのコードをコンパイルしてinsmodしてrmmodします. なんかいい感じに動いたみたい.
$ (mountとか) $ sudo touch -t 202410160900 /mnt/tmp/old $ sudo insmod ./xfs-conv.ko; sudo rmmod xfs-conv; dmesg|grep xfs_conv: [86968.662739] xfs_conv: xfs online convert start [86968.662765] xfs_conv: xfs sb ffff8ad326471000 [86968.662768] xfs_conv: xfs magic 58465342 [86968.662769] xfs_conv: xfs feature 49ff6ab [86968.662771] xfs_conv: BIGTIME enabled [86968.694864] xfs_conv: xfs online convert finish
できたかな?ということで, 同様に"new"を作ってsyncします. それぞれdumpを見ます.
まずは"old"の方. ちゃんと昔のタイムスタンプ.
$ hexdump -C -s $((131 * 512)) -n $((512 * 1)) xfs.img 00010600 49 4e 81 a4 03 02 00 00 00 00 00 00 00 00 00 00 |IN..............| 00010610 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010620 67 0f 02 00 00 00 00 00 67 0f 02 00 00 00 00 00 |g.......g.......| 00010630 67 0f 46 7e 03 cf 92 12 00 00 00 00 00 00 00 00 |g.F~............| 00010640 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010650 00 00 18 01 00 00 00 00 00 00 00 00 e2 73 9e fe |.............s..| ...
そして"new"の方. やったぜ新しいタイムスタンプフォーマットで書かれています.
$ hexdump -C -s $((132 * 512)) -n $((512 * 1)) xfs.img 00010800 49 4e 81 a4 03 02 00 00 00 00 00 00 00 00 00 00 |IN..............| 00010810 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010820 35 cc 2a cf 0b 94 00 00 35 cc 2a cf 0b 94 00 00 |5.*.....5.*.....| 00010830 35 cc 3a cc e7 3c 8c d0 00 00 00 00 00 00 00 00 |5.:..<..........| 00010840 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010850 00 00 18 01 00 00 00 00 00 00 00 00 41 48 72 11 |............AHr.| ...
"old"の方をもう一度touchしてsyncなどします. dumpを見ると…新しいフォーマットに変換されました.
$ sudo touch /mnt/tmp/old $ hexdump -C -s $((131 * 512)) -n $((512 * 1)) xfs.img 00010600 49 4e 81 a4 03 02 00 00 00 00 00 00 00 00 00 00 |IN..............| 00010610 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010620 35 cc 2a cf 0b 94 00 00 35 cc 2a cf 0b 94 00 00 |5.*.....5.*.....| 00010630 35 cc 3a d4 a5 d3 2f 09 00 00 00 00 00 00 00 00 |5.:.../.........| 00010640 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010650 00 00 18 01 00 00 00 00 00 00 00 00 e2 73 9e fe |.............s..| ...
つまり, kernel moduleを書いてbitを立てればいいだけなので, XFSのbigtimeはonline変換できて安心だよということになります.
…というのは言いすぎにしろ, このように(trivialなcaseだけど)実際にonline変換はできるのであるから, そのうちちゃんと実装されるかもしれませんね.
convertできない場合
XFSのbigtime有効化について, offlineでの変換・onlineでの変換を見てきました. しかし, このような変換ができない場合もあります. 現在のXFSのフォーマットはv5ですが, v4以前のフォーマットのものは, このような変換はできません. v4からv5へはバックアップ・リストア以外に方法はないです. v5が出たのは2013年なので, その頃より前のファイルシステムではv4であるかもしれません.
v5では, メタデータにCRCがついたり, 全てのメタデータがself describing metadata (metadata冒頭に"IN"など, マジックが入る)となるなどの変更が入っています. この変更はあまりに大規模なので, バックアップ・リストアになるしかないのでしょう.
ということでみなさん快適な2038年をお過ごしください.
P.S.
こういう話が好きな人は11月9日に富山に行くといいようですよ.
WezTermをはじめてWezTermをやめようかと思うまで
ターミナルでほとんどを過ごす都合がある。しばらく前まではkittyを使っていたのが、このところはWezTermを使っていた。
WezTermは最近はやりのGPU-acceleratedなターミナルエミュレータで機能が豊富である。
たとえば、タブを作れたり、リーダーキー(screenとかのCtrl-a的な感じで他のショートカットのprefixになる)を設定できたり、workspaceを複数作れたり、builtinなsshでリモート接続したり、multiplexできたりする。
これらリッチな機能のおかげで、端末+tmux的な機能と操作感をWezTermだけでできて大変にべんり…という感じだった。…が、いまはやめようかなと思っている。
やめようかなとなっている理由
環境が悪いというとそれまでなのだが、nvidiaのGPU(+Linux)で動かしているせいか、たまにバグる。ひどいときはGUIプロセスが死ぬので、セッション全体的に死ぬ。tmuxにしてれば復帰できたのにね。sleep復帰ごとに毎回死ぬようになってとてもつらい。
また、リモートにsshしたりmultiplexしたり……もできるのだが、versionが一致しないとうまくいかない(厳密にはプロトコルバージョンがあるようだけど)。プロトコルバージョンがそれなりに更新されてる+remoteと手元が別ディストリですとなると動く確率が低下してやってられない。
あと、手元でも操作して、remoteからも同じmultiplexにattachしたい時には、multiplexの設定がtrickyなのもあまりよくない。
結局、multiplexは別ソフトでやる方がいいという結論になる。
やめてどうするか
WezTermでtmux的機能をするのをやめるのも一案だが、それならよりkittyに戻っていいかも。ただkittyはIMEを受けつけないのでそこはよくない。alacrittyを試してみるのもいいかもしれない。
ついでに、tmuxの代わりにもう少しちゃんとzellijを試してみたい。
app-office/libreoffice-24.2.3.2-r1でsandbox violation, なぜ?
ひさしぶりのGentooの話. Xvfbをいれていると, libreofficeのconfigure phaseで, sandbox violationが出て進みません. 実際のところ, 下のようにbugzあるのでそれ見りゃいいんですけど.
sandbox violationはこんな感じ. Xvfbが/dev/udmabufをさわりにいく.
>>> Source configured.
* ----------------------- SANDBOX ACCESS VIOLATION SUMMARY -----------------------
* LOG FILE: "/dev/shm/portage/app-office/libreoffice-24.2.3.2-r1/temp/sandbox.log"
*
VERSION 1.0
FORMAT: F - Function called
FORMAT: S - Access Status
FORMAT: P - Path as passed to function
FORMAT: A - Absolute Path (not canonical)
FORMAT: R - Canonical Path
FORMAT: C - Command LineF: open_wr
S: deny
P: /dev/udmabuf
A: /dev/udmabuf
R: /dev/udmabuf
C: Xvfb :99 -screen 0 1280x1024x24 -nolisten tcp -auth /dev/shm/portage/app-office/libreoffice-24.2.3.2-r1/temp/xvfb-run.ec0CjX/Xautho
rity
* --------------------------------------------------------------------------------
sandbox violationが起きているのは, configure.acでいうとこのへん. AT-SPI2のテストを走らせるのに, gtk3・xvfb-run・dbus-launchが必要なのでそれらのチェックをしている. なのでUSE=gtkが入ってなければ多分 enable_atspi_tests=no になって, $XVFB_RUNの行にもいかず, sandbox violationも起きないでしょう. 多分.
11884 │ # AT-SPI2 tests require gtk3, xvfb-run, dbus-launch and atspi-2
11885 │ if ! test "$ENABLE_GTK3" = TRUE; then
11886 │ if test "$enable_atspi_tests" = yes; then
11887 │ AC_MSG_ERROR([--enable-atspi-tests requires --enable-gtk3])
11888 │ fi
11889 │ enable_atspi_tests=no
11890 │ fi
11891 │ if ! test "$enable_atspi_tests" = no; then
11892 │ AC_PATH_PROGS([XVFB_RUN], [xvfb-run], no)
11893 │ if ! test "$XVFB_RUN" = no; then
11894 │ dnl make sure the found xvfb-run actually works
11895 │ AC_MSG_CHECKING([whether $XVFB_RUN works...])
11896 │ if $XVFB_RUN --auto-servernum true >&AS_MESSAGE_LOG_FD 2>&AS_MESSAGE_LOG_FD; then
11897 │ AC_MSG_RESULT([yes])
11898 │ else
11899 │ AC_MSG_RESULT([no])
11900 │ XVFB_RUN=no
11901 │ fi
11902 │ fi
11903 │ if test "$XVFB_RUN" = no; then
11904 │ if test "$enable_atspi_tests" = yes; then
11905 │ AC_MSG_ERROR([xvfb-run required by --enable-atspi-tests not found])
11906 │ fi
11907 │ enable_atspi_tests=no
11908 │ fi
11909 │ fi
まあ, gtkをこれのためにdisableするのは困るって話なんですが.
ところで, 結局これでどうなるかというと, 変数名がしめすように, ATSPIのunittestが走るだけなんですよね.
86 │ ifneq ($(ENABLE_ATSPI_TESTS),)
87 │ $(eval $(call gb_Module_add_check_targets,vcl,\
88 │ CppunitTest_vcl_gtk3_a11y \
89 │ ))
90 │ endif
一方でebuildの方を見てみると, 全般的にcheckは落とされている様子. まあビルドしたいだけなので. ということは, 上の判定コード自体全部消してもいいんじゃないかなあ?
app-office/libreoffice/libreoffice-24.2.3.2-r1.ebuild
378 │ # sed in the tests
379 │ sed -i \
380 │ -e "s#all : build unitcheck#all : build#g" \
381 │ solenv/gbuild/Module.mk || die
382 │ sed -i \
383 │ -e "s#check: dev-install subsequentcheck#check: unitcheck slowcheck dev-install subsequentcheck#g" \
384 │ -e "s#Makefile.gbuild all slowcheck#Makefile.gbuild all#g" \
385 │ Makefile.in || die
実際のところ, 同じくsandbox violationが起きるからと, kf5-configのところも消されているのだ.
369 │ # sandbox violations on many systems, we don't need it. Bug #646406
370 │ sed -i \
371 │ -e "/KF5_CONFIG/s/kf5-config/no/" \
372 │ configure.ac || die "Failed to disable kf5-config"
それだけでなく, ebuildを見ると, libreofficeはビルド中に結構いろんなとこをさわりにくるのがわかる. OpenGLがどのぐらい使えるのかを見てるのかなあ? configureの後で? それか, ビルドした実行バイナリを実際動かして, データの後処理的なことをしているのかも. emacsのbuildみたいに.
602 │ # more and more LO stuff tries to use OpenGL, including tests during build
603 │ # bug 501508, bug 540624, bug 545974 and probably more
604 │ addpredict /dev/dri
605 │ addpredict /dev/ati
606 │ addpredict /dev/nvidiactl
まとめ: 巨大ソフトのビルドはめんどい
現代日本語書き言葉均衡コーパス(BCCWJ)で漢直(T-Code)を研究する
ゆる言語学ラジオというpodcastを聞いていたところ, 「現代日本語書き言葉均衡コーパス」(BCCWJ)というものが紹介されていました. これは「書籍全般、雑誌全般、新聞、白書、ブログ、ネット掲示板、教科書、法律などのジャンルにまたがって1億430万語のデータを格納しており、各ジャンルについて無作為にサンプルを抽出」したコーパスとのことです.
さて, やはり漢字というのはなるべく変換しない方がいいわけですが, その解決策として漢字直接入力(漢直)というのがあります. たとえば, T-Codeという入力法では2打鍵の組み合わせで, 1つのひらがな・カタカナ・数学・記号・漢字が入力されます.
こうした漢直で問題になるのが, その打鍵の配列の評価です. なるべくホームポジション近くで, よく出現する文字を入力できれば, それは効率のよい配置と言えます. たとえば, T-Codeでは"kd"(QWERTY)で「の」と入力できますが, 「の」の出現率が高ければこれは妥当だということです.
ここでBCCWJの登場です. BCCWJには文字表というものがあり, これには各文字の出現頻度・100万字あたりの頻度が書かれています. これを使えば, ある漢直がどの程度の日本語文章を入力できるのかを評価できるはずです.
では, やってみましょう…とはいえ, ある打鍵の組がどれだけホームポジションに近いのか…は自明ではありません. そこで今回は漢直の練習テキストEELLLを代わりの指標として使います. この練習テキストは, いくつかのレッスンに分かれており, 最初は「の」や「、」から始まり, ひらがな・数字・カタカナ・漢字…と少しずつ新たな文字を導入していきます. レッスンに早く登場する文字はそれだけ重要だと考えられているはずです. つまり, 各文字がEELLLの中で何番目に出現するのかの順位を代替的な指標として使います.
EELLL内の順位(縦)と, BCCWJ文字表(Version 1.1)で100万文字あたりの頻度での順位(横)を比較してプロットします.

理想的にはy=xの直線状になるといいですが, そうはなっていませんね. これを見ると右上と左下はいい感じだが, 中間はびみょうな感じがします. つまり, 400文字ぐらいまでは出現頻度が高い文字が早めのレッスンに出ますし, 逆に頻度が低いものは後のレッスンになっています. その間はちょっとばらばらな感じです. また, 右下にちょこちょこ外れている子たちがいますね.
では, どんな文字がEELLLでの順位とBCCWJでの順位の乖離が大きいのでしょうか?
| char | lesson | rank_eelll | freq | rank_bccwj | norm_sq | |
|---|---|---|---|---|---|---|
| 129 | ぢ | 105 | 129 | 8.46803 | 1154 | 1050625 |
| 133 | ぺ | 106 | 131 | 18.15968 | 1144 | 1026169 |
| 131 | ぴ | 106 | 131 | 34.50698 | 1100 | 938961 |
| 132 | ぷ | 106 | 131 | 40.06188 | 1078 | 896809 |
| 127 | ゅ | 104 | 124 | 57.65840 | 998 | 763876 |
| 281 | 遇 | 305 | 282 | 25.28123 | 1124 | 708964 |
| 134 | ぽ | 106 | 131 | 72.06532 | 923 | 627264 |
| 338 | 即 | 311 | 335 | 40.59946 | 1077 | 550564 |
| 361 | 巨 | 314 | 362 | 46.38987 | 1050 | 473344 |
| 434 | 塚 | 323 | 434 | 27.55950 | 1119 | 469225 |
| 426 | 序 | 322 | 423 | 34.73737 | 1099 | 456976 |
| 469 | 熟 | 404 | 464 | 29.11078 | 1112 | 419904 |
| 433 | 浦 | 323 | 434 | 39.51919 | 1081 | 418609 |
| 342 | 募 | 311 | 335 | 61.56475 | 978 | 413449 |
| 370 | 華 | 315 | 371 | 56.05592 | 1004 | 400689 |
| 327 | 岩 | 310 | 326 | 67.49340 | 948 | 386884 |
| 415 | 羽 | 321 | 414 | 51.53520 | 1031 | 380689 |
| 458 | 卒 | 403 | 454 | 46.06733 | 1054 | 360000 |
| 372 | 迎 | 315 | 371 | 63.41297 | 966 | 354025 |
| 690 | 言 | 505 | 688 | 1363.83967 | 96 | 350464 |
「ぢ」「ぺ」「ぴ」などが上位にあります. これらはひらがななので初期レッスンには出るものの実際のところ, あまり登場しないということです. たしかにね. というか, 告白すると自分も「ぢ」についてはあまりに入力しないのでT-Codeでの入力方法がわかりません. これらのrank_eelllが130ぐらいで, rank_bccwjが1100ぐらい…つまり上のプロットの右下の点はこいつらということです.
漢字でいうと「遇」「即」「巨」があります. これらは…いやーあんまりよくわかりませんね…まあ出現しないわりにレッスンでは早いというところです.
上の表は多くの文字が(EELLL順位) < (BCCWJ順位)というものでした. つまり, レッスンが早すぎるものたちです. 逆にレッスンが遅すぎる - EELLLで過少評価されている文字を見てみましょう.
| char | lesson | rank_eelll | freq | rank_bccwj | norm_sq | |
|---|---|---|---|---|---|---|
| 690 | 言 | 505 | 688 | 1363.83967 | 96 | 350464 |
| 970 | 存 | 654 | 969 | 269.56401 | 412 | 310249 |
| 842 | 条 | 525 | 842 | 385.98666 | 311 | 281961 |
| 1034 | 識 | 661 | 1023 | 220.36853 | 492 | 281961 |
| 1094 | 離 | 670 | 1086 | 183.78294 | 571 | 265225 |
| 687 | 何 | 505 | 688 | 778.37298 | 174 | 264196 |
| 839 | 得 | 524 | 837 | 353.41494 | 332 | 255025 |
| 879 | 域 | 602 | 880 | 295.75143 | 378 | 252004 |
| 928 | 笑 | 605 | 923 | 253.58021 | 438 | 235225 |
| 1073 | 療 | 663 | 1059 | 182.12414 | 576 | 233289 |
| 1102 | 願 | 676 | 1098 | 161.55307 | 616 | 232324 |
| 1032 | 呼 | 661 | 1023 | 197.30926 | 545 | 228484 |
| 828 | 認 | 522 | 826 | 324.38607 | 349 | 227529 |
| 1047 | 君 | 662 | 1042 | 184.32563 | 568 | 224676 |
| 1025 | 影 | 661 | 1023 | 189.15353 | 557 | 217156 |
| 678 | 等 | 504 | 673 | 559.53526 | 224 | 201601 |
| 1003 | 移 | 659 | 1003 | 189.24569 | 556 | 199809 |
| 759 | 質 | 512 | 758 | 374.09353 | 316 | 195364 |
| 751 | 確 | 511 | 752 | 381.80896 | 313 | 192721 |
| 825 | 申 | 522 | 826 | 287.10420 | 389 | 190969 |
「言」「何」「得」などたしかに結構入力しそうな文字がありますが, これらはどれもレッスン全体の後半での出現です. 個人的にはプログラミング系のことを書いていると頻出する「呼」がちゃんとここに入っていて良かったです.
さて, 今回はBCCWJ文字表(Version 1.1)を使ってT-Codeの配列について研究してみました. この文字表を使うことで, 他にもEELLLでどのレッスンまでいけば文章中で何割の文字を入力できるようになるのか? 何文字の打鍵を覚えれば文章の何割を直接入力できるのか?が計測できます. それらは次回になります.
Waylandで日本語入力への道: Return of RVO編
Kernel/VM Advent Calendarの何日目かの記事です.
- 第1回 Waylandで日本語入力への道: 下調べ編 - Gentoo metalog
- 第2回 Waylandで日本語入力への道: vtableを作ろう編 - Gentoo metalog
- 第3回 Waylandで日本語入力への道: コンストラクタに届け編 - Gentoo metalog
- 第4回 Waylandで日本語入力への道: dynamic_castがならなくて編 - Gentoo metalog
- 第5回 Waylandで日本語入力への道: std::string の捏造編 - Gentoo metalog
前回のあらすじ: std::string(C++)のことを調べて, Rustで作れるようになった. でも, 小さい文字列用に最適化された状態にはできない… ちくしょう! アセンブラに相談だ!
DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.
文字列をやってるとだいぶ日本語入力に近付いてきた気持ちになるこのシリーズいかがおすごしでしょうか.
RVO?なんですか?
さて前回は, std::string(C++)の小さい文字列用フォーマットを作りたかったものの挫折したわけですが…. このへんをいろいろ調べていくと, Return Value Optimization (RVO)による現象なのかなあとわかってきました.
せっかくなので, このへん深掘りしておきましょう. 小さいC++プログラムを作ってやってみましょう. メインはこんな感じ.
#include <string> #include <iostream> extern "C" { std::string f(); } int main() { std::string s = f(); std::cout << s << std::endl; printf("s in main() is at %p\n", &s); printf(" buffer address: %p\n", *(void**)&s); return 0; }
これに対して, C++によるf()の実装とRustによるf()の実装を作ります. C++版はこうなります.
extern "C" std::string f() { std::string foo = ""; printf("foo in f() is at %p\n", &foo); printf(" buffer address: %p\n", *(void**)&foo); return foo; }
RustでSSOしないと, こんな感じ
#[no_mangle] pub unsafe extern "C" fn f() -> CxxString { let buffer: Box<[u8]> = Box::new([0; 32]); let ptr = Box::into_raw(buffer) as *const _; println!("string buffer is at {:?}", ptr); CxxString { ptr: ptr, size: 0, capacity: 32, pad: [0; 8], } }
で, これらのアセンブラを見ます. まずはmain()から.
11b6: 48 89 e5 mov %rsp,%rbp ... std::string s = f(); 11cd: 48 8d 45 c0 lea -0x40(%rbp),%rax 11d1: 48 89 c7 mov %rax,%rdi 11d4: e8 77 fe ff ff call 1050 <f@plt>
ここはf()を呼んでいるところですが, %rdiレジスタに"-0x40(%rbp)"が入っています. ここで%rbp=%rspなので, まあスタック上のアドレス(std::string sのアドレス)が, f()への隠し引数として渡されているわけです.
C++版とRust版では, この引数の扱いが変わってきます. C++版のf()を見ましょう.
22de: 48 89 7d d8 mov %rdi,-0x28(%rbp) # -0x28(%rbp) = %rdi = 隠し引数 = "mainのs" ... 2301: 48 8b 45 d8 mov -0x28(%rbp),%rax 2305: 48 8d 0d f4 0c 00 00 lea 0xcf4(%rip),%rcx # 3000 <_fini+0x928> 230c: 48 89 ce mov %rcx,%rsi # 多分文字列 "" 230f: 48 89 c7 mov %rax,%rdi # callの第一引数: "mainのs" 2312: e8 69 fe ff ff call 2180 <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC1IS3_EEPKcRKS3_@plt> # call されるのはstd::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)
ここでcallされているのは, std::stringのコンストラクタです. 結局, main内の"std::string s"に対して, f()の中から直接, 文字列("")を引数にとるコンストラクタを呼んでいます.
では, Rust版ではどうなっているのでしょう.
7847: 48 89 7c 24 08 mov %rdi,0x8(%rsp) ... 78c6: e8 85 f9 ff ff call 7250 <_ZN5alloc5boxed16Box$LT$T$C$A$GT$8into_raw17h9f973d5138f7f2c2E> 78cb: 48 89 44 24 48 mov %rax,0x48(%rsp) # 0x48(%rsp) = Box::into_raw() ... 7951: 48 8b 4c 24 08 mov 0x8(%rsp),%rcx 7956: 48 8b 44 24 10 mov 0x10(%rsp),%rax CxxString { ptr: ptr, # ここから返り値へのコピー 795b: 48 8b 54 24 48 mov 0x48(%rsp),%rdx # %rdx = 0x48(%rsp) = Box::into_raw() size: 0, capacity: 32, pad: [0; 8], 7960: 48 c7 84 24 90 00 00 movq $0x0,0x90(%rsp) 7967: 00 00 00 00 00 CxxString { 796c: 48 89 11 mov %rdx,(%rcx) # ptr = %rdx = 0x48(%rsp) = Box::into_raw() 796f: 48 c7 41 08 00 00 00 movq $0x0,0x8(%rcx) # size = 0 7976: 00 7977: 48 c7 41 10 20 00 00 movq $0x20,0x10(%rcx) # capacity = 32 797e: 00 797f: 48 8b 94 24 90 00 00 mov 0x90(%rsp),%rdx 7986: 00 7987: 48 89 51 18 mov %rdx,0x18(%rcx) # pad
Rust版のf()でも, 返り値となる構造体にBoxで確保したバッファやcapacityなどを直接コピーしています. ここで最適化されて直接構築されるのがある意味で問題で, たとえば一時オブジェクトがポインタで返っていってそれが呼び出し側でコピーコンストクタなり呼ばれなおす…という感じならもしかしてうまくいっていたのかもしれません.
結局のところ, 隠し引数のアドレスをとってきて, そのアドレスを元に適切なデータを構築する必要があります. ということは, このrdiレジスタに来る隠し引数をとる方法がなにかあれば…なにか…なにかないのか……?
asm!()でい!
レジスタには来てるんだから, asmでとればいいじゃないという話ですね. ということで, こんな感じ
let rdi: usize; unsafe { asm!("mov {}, rdi", out(reg) rdi) }; eprintln!("rdi = {:#x}", rdi); CxxString { ptr: (rdi + 16) as *const _, size: 3, capacity: 0x6f6f66, // "foo" pad: [0; 8], }
asm!()でレジスタrdiの値をとって, そこから最適化状態のstd::stringのバッファ位置を計算します. capacityのところに, 文字列になるように値を設定すれば…
rdi = 0x7ffd92b51c40 foo s in main() is at 0x7ffd92b51c40 buffer address: 0x7ffd92b51c50
やったね, 動きました.
ところで, 返り値の書きこみ先がわかったなら, もっと直接的に書く感じのコードでもいいのでは? たとえばこんな
#[repr(C)] struct CxxShortString { ptr: *const u8, size: u64, buffer: [u8; 16], } #[no_mangle] pub unsafe extern "C" fn f() { let rdi: *mut CxxShortString; unsafe { asm!("mov {}, rdi", out(reg) rdi) }; eprintln!("rdi = {:?}", rdi); let s = "foo"; (*rdi).ptr = &(*rdi).buffer as *const _; (*rdi).size = 3; (*rdi).buffer[..s.len()].copy_from_slice(s.as_bytes()); }
これもいい感じに動きました. どっちがRustっぽいでしょうね. asm!()な時点で終わりっちゃ終わりか.
asm!()を使ったりバイナリ形式を仮定したり, ひたすらプラットフォーム依存な感じですが, そういうとこ遊べるのも自作IMEのいいところということでここはひとつ. これで最適化されたstd::string(C++)を作れるようになりました. しかし, これ使いづらいなあ…
ということで, 貯まってたネタがなくなってきたので, 次回はどうだろう…多分
次回: Waylandで日本語入力への道: Preeditを作ろう編
かなあ…
Waylandで日本語入力への道: std::string の捏造編
Kernel/VM Advent Calendarの何日目かの記事です.
- 第1回 Waylandで日本語入力への道: 下調べ編 - Gentoo metalog
- 第2回 Waylandで日本語入力への道: vtableを作ろう編 - Gentoo metalog
- 第3回 Waylandで日本語入力への道: コンストラクタに届け編 - Gentoo metalog
- 第4回 Waylandで日本語入力への道: dynamic_castがならなくて編 - Gentoo metalog
前回のあらすじ: 関数だけ並べたvtableでさぼっていたら, しっかりdynamic_cast が動かなくて怒られた. bindgenもそうしてるんだけどなあ…. しょうがないので, ちゃんと型情報を入れてあげました.
DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.
そろそろ忘れられているかもしれませんが, このシリーズはWaylandで日本語入力をするために行われています. 今回は std::string のbinary formatを調べて, それをRustで作りあげて C++ (fcitx5)に返します. 日本語を入力するというのは, こういった作業の先にあるもので, かくも大変なものなのですね. 日本語をしっかり入力していきましょう.
std::string の中身ってどうなってるの?
さっそく std::string のbinary formatを調べましょう. ぐぐってるとこういう記事が見つかりました.
これを見ると, std::stringは基本形態では図の左のように3つのフィールド buffer・size・capacityを持つことがわかります. bufferがheap上で文字列を保持するバッファを指し, sizeは文字列の長さ, capacityはbufferのサイズです. (x86_64 linuxでは)
一方, 文字列のサイズが小さい場合には, その管理のために24byteを使うのは無駄があります. そこでSmall String Optimization (SSO)として, 図の右側のように, 構造体の内部に文字列のデータを持っておきます. この時, capacityは32byteで固定ということなのでしょう.

gdbで確認じゃ
せっかくなので実際にコードを書いて確認しておきましょう. 以下のようなコードを書いて, "g++ -O0 -ggdb"でコンパイルしてgdbでcoutの行で止めます.
#include <string> #include <iostream> std::string f() { std::string tmp = "foo"; tmp.reserve(32); return tmp; } int main() { std::string s = f(); std::cout << s << std::endl; return 0; }
さて見てみますと, たしかに最初の8byteが文字列のバッファを指していて, その次に文字列サイズの3, そして次にバッファサイズの0x20=32が入っています.
(gdb) n 11 std::string s = f(); (gdb) 12 std::cout << s << std::endl; (gdb) x/4xg &s 0x7fffffffd7a0: 0x000055555556c2b0 0x0000000000000003 0x7fffffffd7b0: 0x0000000000000020 0x0000000000000000 (gdb) x/s 0x000055555556c2b0 0x55555556c2b0: "foo" (gdb) p sizeof(s) $1 = 32
では, ここで"tmp.reserve()"の行を削除するとどうなるでしょう. Small String Optimizationがきかなくなるので, 文字列"foo"がsの内部に埋め込まれます.
(gdb) n 12 std::cout << s << std::endl; (gdb) x/4xg &s 0x7fffffffd7a0: 0x00007fffffffd7b0 0x0000000000000003 0x7fffffffd7b0: 0x00000000006f6f66 0x0000000000000000 (gdb) x/s 0x7fffffffd7b0 0x7fffffffd7b0: "foo"
Rustでstd::string(C++)を作ろう
これらをふまえて, Rustでstd::string(ただしC++)を作って返しましょう. Rustのなら一発なのに.
こんな感じで標準タイプのstd::string(C++)の形を作って
#[repr(C)] struct CxxString { ptr: *const u8, size: u64, capacity: u64, pad: [u8; 8], }
適当に32byteのバッファを作って CxxStringの構造体に持たせて, capacityを32にしておきます. バッファは(多分)C++がstd::stringが死ぬ時に解放してくれる…はずなのでOK.
let buffer: Box<[u8]> = Box::new([0; 32]); CxxString { ptr: Box::into_raw(buffer) as *const _, size: 0, capacity: 32, pad: [0; 8], }
ということで動かすと, 今度は"sub_mode()"のtodo!()に当たって死にました. こっちは上記の右側の構造でやってみたいところです.
RustでもSSOしよう
として, したかったんですが一度挫折しました. 挫折したコードたち
let x = Box::new(CxxString { ptr: std::ptr::null_mut(), size: 0, capacity: 0, pad: [0; 8], }); let ptr = Box::into_raw(x); let buf = (ptr as u64) + 16; let mut x = Box::from_raw(ptr); x.ptr = buf as *const _; let ptr = Box::into_raw(x); *ptr
let mut x = CxxString { ptr: std::ptr::null_mut(), size: 0, capacity: 0, pad: [0; 8], }; let ptr = &mut x as *mut _; x.ptr = ((ptr as u64) + 16) as *const _; x
なにをどうしてもdouble freeぽくなったりしてます.
たとえば下のコードだと関数の出口で
(gdb) p &x $1 = (*mut tcode::CxxString) 0x7fffffffce80 (gdb) x/4xg $1 0x7fffffffce80: 0x00007fffffffce90 0x0000000000000000 0x7fffffffce90: 0x0000000000000000 0x0000000000000000
こんな感じで, SSOされたバイナリができています. これが返っていって
(gdb) n fcitx::InstancePrivate::showInputMethodInformation (this=this@entry=0x5555555d2ea0, ic=ic@entry=0x555555e7c700) at /dev/shm/portage/app-i18n/fcitx-5.1.5/work/fcitx5-5.1.5/src/lib/fcitx/instance.cpp:391 391 auto subModeLabel = engine->subModeLabel(*entry, *ic); (gdb) p &subMode $2 = (std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > *) 0x7fffffffcf10 (gdb) x/4xg &subMode 0x7fffffffcf10: 0x00007fffffffce90 0x0000000000000000 0x7fffffffcf20: 0x0000000000000000 0x0000000000000000
こう, 本当にそのまま値がコピーされてるんですよね. 結果として, std::string(C++)のデスクトラクタがこれはSSOではないなと思って, 0x00007fffffffce90をfree()しにいく. すると, そこはallocした先頭のアドレスではないのでinvalid pointerで死ぬと見えます.
一方C++だとなんだかmainのstack上で直接いじられているように見える.
なんでだろ〜となった時, 困った時は逆アセンブルですよね. ということで
次回: Waylandで日本語入力への道: Return of RVO編