ファイルシステムソムリエになる話
利きファイルシステムができるのかという話を見かけたので、できそうだなと思って書いたやつです。
「ここに何かのファイルシステムがあります。ファイル操作を行って、どのファイルシステムか当ててください。なおディスクイメージを見ることはできません。」という問題を解く方法について考えます。
ぱっと思いつく方法はこれでしょうからやっていきます
ディレクトリのなかにファイル作っていってinode番号見ればbtrfsとext4とXFSは区別できる気がしますね
— シャルロット・びーる尽き太郎の憂鬱 (@naota344) 2016年6月15日
以下のスクリプトを走らせます。512GBで各FSのイメージ作って、ディレクトリをいくつか掘ってinode番号を表示させるだけです。
#!/bin/sh FS="btrfs ext4 xfs" DIR=/mnt/test for f in $FS; do echo ${f} truncate -s 512G ${f}.img && mkfs.${f} ${f}.img >/dev/null && mount ${f}.img ${DIR} && mkdir -p ${DIR}/{0,1}/{0,1,2,3} && ls -ldi ${DIR}/{0,1}/{,0,1,2,3} && umount /mnt/test && rm ${f}.img done
結果はこんな感じ:
btrfs 257 drwxr-xr-x 1 root root 8 Jun 16 23:36 /mnt/test/0/ 258 drwxr-xr-x 1 root root 0 Jun 16 23:36 /mnt/test/0/0 259 drwxr-xr-x 1 root root 0 Jun 16 23:36 /mnt/test/0/1 260 drwxr-xr-x 1 root root 0 Jun 16 23:36 /mnt/test/0/2 261 drwxr-xr-x 1 root root 0 Jun 16 23:36 /mnt/test/0/3 262 drwxr-xr-x 1 root root 8 Jun 16 23:36 /mnt/test/1/ 263 drwxr-xr-x 1 root root 0 Jun 16 23:36 /mnt/test/1/0 264 drwxr-xr-x 1 root root 0 Jun 16 23:36 /mnt/test/1/1 265 drwxr-xr-x 1 root root 0 Jun 16 23:36 /mnt/test/1/2 266 drwxr-xr-x 1 root root 0 Jun 16 23:36 /mnt/test/1/3 ext4 mke2fs 1.42.13 (17-May-2015) 2097153 drwxr-xr-x 6 root root 4096 Jun 16 23:56 /mnt/test/0/ 2097154 drwxr-xr-x 2 root root 4096 Jun 16 23:56 /mnt/test/0/0 2097155 drwxr-xr-x 2 root root 4096 Jun 16 23:56 /mnt/test/0/1 2097156 drwxr-xr-x 2 root root 4096 Jun 16 23:56 /mnt/test/0/2 2097157 drwxr-xr-x 2 root root 4096 Jun 16 23:56 /mnt/test/0/3 7077889 drwxr-xr-x 6 root root 4096 Jun 16 23:56 /mnt/test/1/ 7077890 drwxr-xr-x 2 root root 4096 Jun 16 23:56 /mnt/test/1/0 7077891 drwxr-xr-x 2 root root 4096 Jun 16 23:56 /mnt/test/1/1 7077892 drwxr-xr-x 2 root root 4096 Jun 16 23:56 /mnt/test/1/2 7077893 drwxr-xr-x 2 root root 4096 Jun 16 23:56 /mnt/test/1/3 xfs 99 drwxr-xr-x 6 root root 42 Jun 16 23:36 /mnt/test/0/ 268435552 drwxr-xr-x 2 root root 6 Jun 16 23:36 /mnt/test/0/0 537395296 drwxr-xr-x 2 root root 6 Jun 16 23:36 /mnt/test/0/1 805306464 drwxr-xr-x 2 root root 6 Jun 16 23:36 /mnt/test/0/2 100 drwxr-xr-x 2 root root 6 Jun 16 23:36 /mnt/test/0/3 268435553 drwxr-xr-x 6 root root 42 Jun 16 23:36 /mnt/test/1/ 537395297 drwxr-xr-x 2 root root 6 Jun 16 23:36 /mnt/test/1/0 805306465 drwxr-xr-x 2 root root 6 Jun 16 23:36 /mnt/test/1/1 101 drwxr-xr-x 2 root root 6 Jun 16 23:36 /mnt/test/1/2 268435554 drwxr-xr-x 2 root root 6 Jun 16 23:36 /mnt/test/1/3
わかりやすいですね。 btrfsはinode番号がシーケンシャルに増えていきます。ext4とXFSではinode番号が飛んでいます。ext4とXFSで比べるとext4では同じディレクトリ内のファイル(0/{,0,1,2,3}の組および1/{,0,1,2,3})はinode番号が固まっています。一方、XFSでは同一ディレクトリ内のファイルでもinode番号が飛んでいます。
こうしたinode番号のallocのされ方の違いは各FSのレイアウト・allocation policyに起因しています。詳しく見ていきましょう。
Ext4
Ext4はファイルシステム内部がBlock Group(BG)という領域に分割されています。各BGは空きブロックの数・空きinodeの数・どのinodeが空いているのかを示すbitmapを持ちます。inodeのallocateはどこかのBGから行われます。
たとえば今回の結果だと、"0/*"がBG#256から、"1/*"がBG#864からallocされていることがわかります。"Group 256"の"Free inodes"が"2097158-"と"0/3"の次のinode番号になり、同様に"Group 864"でも"Free inodes"が"1/3"の次である"7077894-"となっています。
# dumpe2fs ext4.img … Group 256: (Blocks 8388608-8421375) [ITABLE_ZEROED] Checksum 0xb0b5, unused inodes 8187 Block bitmap at 8388608 (+0), Inode bitmap at 8388624 (+16) Inode table at 8388640-8389151 (+32) 24539 free blocks, 8187 free inodes, 5 directories, 8187 unused inodes Free blocks: 8396837-8421375 Free inodes: 2097158-2105344 … Group 864: (Blocks 28311552-28344319) [ITABLE_ZEROED] Checksum 0xf815, unused inodes 8187 Block bitmap at 28311552 (+0), Inode bitmap at 28311568 (+16) Inode table at 28311584-28312095 (+32) 24539 free blocks, 8187 free inodes, 5 directories, 8187 unused inodes Free blocks: 28319781-28344319 Free inodes: 7077894-7086080
Ext4は新しいinodeを(ルート直下の場合を除いて)親ディレクトリと同じBG内にallocしようとします。、一般にディレクトリのinodeへのアクセスの後にはその中のファイルにアクセスすることが予想されます。ext4では同一BG内のinodeは固まって配置されているため、同一BGにallocすることでディレクトリinodeの読みこみがついでにその中のファイルのinodeの読みこみもやってくれる可能性が高まります。おそらくこの効果による速度向上を狙ったinode allocation policyなのでしょう。まとめるとext4は"locality"を重視したallocationを行っているわけです。
XFS
XFSでもExt4のBGと同様にファイルシステムの内部をAllocation Groupという領域に分割しています。ただし、Ext4では上記のように800以上にも分割されていましたが、XFSでは多くの場合(FSが128MBから4TBの間なら)、4つのAGに分割されるだけです。
そう思ってよくよくXFSのinode番号の結果を見てみると、4つごとにinode番号のだいたいの大きさがそろっていますね。それぞれAG#0-AG#3にallocationされているというわけです。
せっかくなので、こっちもallocationされている様子を見ておきましょう。まず、AG#0の情報をdumpします。
# xfs_db xfs.img xfs_db> agi 0 xfs_db> p magicnum = 0x58414749 versionnum = 1 seqno = 0 length = 33554432 count = 64 root = 3 level = 1 freecount = 58 newino = 96 dirino = null unlinked[0-63] = uuid = 3dd1af10-10f6-45dc-b466-c10a727d9842 lsn = 0x100000002 crc = 0x7df3830f (correct) free_root = 4 free_level = 1
ここの"root"がこのAGでinodeを管理しているB+treeのrootがあるブロックです。該当部分をinodeのB+treeとしてdumpします。
xfs_db> fsblock 3 xfs_db> type inobt xfs_db> p magic = 0x49414233 level = 0 numrecs = 1 leftsib = null rightsib = null bno = 24 lsn = 0x100000002 uuid = 3dd1af10-10f6-45dc-b466-c10a727d9842 owner = 0 crc = 0x1b9f870b (correct) recs[1] = [startino,freecount,free] 1:[96,58,0xffffffffffffffc0]
"[startino,freecount,free] 1:[96,58,0xffffffffffffffc0]" がinode管理情報です。XFSではinodeは64個の塊で動的にallocされていきます。このエントリはinode番号96から64個分のinodeについて管理しています。"freecount"が58なので64-58=6個のinodeが使用されており、どこが空いているのかは"free" bitmapを見ることでわかります。実際96-101に相当する下位6bitが0で残りが1になっていることが見てとれます。
同様にAG#1のinode B+treeも見てみると…。(agi 1+addrでAG#1のoffsetを確認し、そこに3足したところがAG#1のinode B+tree root)
xfs_db> agi 1 xfs_db> addr current byte offset 137438954496, length 512 buffer block 268435458 (fsbno 33554432), 1 bb inode -1, dir inode -1, type agi xfs_db> fsblock 33554435 xfs_db> type inobt xfs_db> p magic = 0x49414233 level = 0 numrecs = 1 leftsib = null rightsib = null bno = 268435480 lsn = 0x100000002 uuid = 3dd1af10-10f6-45dc-b466-c10a727d9842 owner = 1 crc = 0x687eec0e (correct) recs[1] = [startino,freecount,free] 1:[96,61,0xfffffffffffffff8]
こちらもinode番号96から、となっていますね。ここに記録される96というのはAG内localのinode番号でFSでのinode番号にするには上位bit領域にAGの番号を入れる必要があります。そう思って"0/0"のinode番号を見ると"268435552"というのは"0x10000060"というわけで"0x60=96"と"0x1"=AGの番号が見てとれます。
さてXFSのinode allocation policyの話に行きましょう。XFSではディレクトリの場合、FS全体でラウンドロビンにAGを(優先して)選択していきます。そのため最初の実験結果のようにディレクトリごとにAGが変わっていっているわけですね。一方でファイルであればディレクトリと同じAGを優先するので、さきほどのスクリプトをディレクトリ0,1以下はファイルを作るようにするとこうなります。
xfs 96 drwxr-xr-x 4 root root 24 Jun 17 01:18 /mnt/test 99 drwxr-xr-x 2 root root 42 Jun 17 01:18 /mnt/test/0/ 100 -rw-r--r-- 1 root root 0 Jun 17 01:18 /mnt/test/0/0 101 -rw-r--r-- 1 root root 0 Jun 17 01:18 /mnt/test/0/1 102 -rw-r--r-- 1 root root 0 Jun 17 01:18 /mnt/test/0/2 103 -rw-r--r-- 1 root root 0 Jun 17 01:18 /mnt/test/0/3 268435552 drwxr-xr-x 2 root root 42 Jun 17 01:18 /mnt/test/1/ 268435553 -rw-r--r-- 1 root root 0 Jun 17 01:18 /mnt/test/1/0 268435554 -rw-r--r-- 1 root root 0 Jun 17 01:18 /mnt/test/1/1 268435555 -rw-r--r-- 1 root root 0 Jun 17 01:18 /mnt/test/1/2 268435556 -rw-r--r-- 1 root root 0 Jun 17 01:18 /mnt/test/1/3
ディレクトリをFS全体でラウンドロビンに配置していくことで、個々のディレクトリ内の操作はAGが異なれば独立して進めることができ、scalabiltyが向上しています。このようにXFSでは"scalability"を意識したallocation policyが使われています。
Btrfs
Btrfsでは、ディレクトリとファイルとの親子関係・inodeデータ・ファイルのデータ位置情報が全て「ファイルシステムツリー」(FS tree)と呼ばれるB-treeで管理されます。また、Btrfsにおいては「どこのinodeが空いているか」を管理するまとまった情報(ext4やXFSのbitmapみたいな)は(デフォルトでは)存在しません。treeの中でファイル削除などで空いてしまったinode番号を発見する最速の方法はFS treeを全部scanすることです。前述の通り、FS treeには様々な情報が載っているのでこれをなめていくのはかなりの時間がかかります。ということで、Btrfsではこれまでにallocした最大のinode番号を覚えておいて、これまでに割り当てたことのないinode番号を割り当てるというallocation policyをとっています。そのため、最初の結果のようにシーケンシャルになっています。なんですかね、"simple"なallocation policyとでも言っておくといいんでしょうか。
(Btrfsはext4, XFSみたいにFS領域分割しないのかというのがありますが、一応chunkという単位に分かれています。ただ、localityやscalabilityのためでは(多分)なく管理しやすさぐらい…。localityなんてやろうとしてもどうせCoW FSだからそのうち崩壊するだろうし、デフォルトでinode番号管理構造ないからallocするinode番号を開けていくようにするのはうまくいかないし…。inode_cacheを仮定してallocするinode番号を変えるなどするのはおもしろいのかもしれないが…もう2時なのでもうこんな感じで)
まとめ
それぞれのFSがBtrfsはsimple, Ext4はlocality, XFSはscalabilityと違ったinode allocation policyを取っています。そのおかげであなたも明日からファイルシステムソムリエです。おめでとうございます。
補則
今回は作りたてのFSだったけれど、じゃあ、ある程度使われたFSだとどうなるってことなんですが、inode_cacheなしのbtrfsだとやはりシーケンシャルに当たるのでわかりやすい。ext4とXFSではinodeの空き方によって複雑になる気がします。まあXFSの方が性質上inode番号が大きくなりがちなんでそこ見ていけばいいかなとか、inode領域が動的確保なことを利用してあるAG内のファイルにデータを書きまくる->その後にそのAG内でallocされたinode番号を見るといったことをすればいいんじゃないでしょうか。
あとbtrfs、inode割り当て一周したらどーすんのってのはちゃんとinode_cacheというmount optionでどこが空いているかのデータベース作ってそこ見てくれるようになるんで安心してください。でも64bit環境ならそうそうなくならんし普通はなくてもいいんじゃないかな。
おまけ: f2fsとnilfs2
f2fs 4 drwxr-xr-x 6 root root 4096 Jun 17 02:20 /mnt/test/0/ 5 drwxr-xr-x 2 root root 4096 Jun 17 02:20 /mnt/test/0/0 6 drwxr-xr-x 2 root root 4096 Jun 17 02:20 /mnt/test/0/1 7 drwxr-xr-x 2 root root 4096 Jun 17 02:20 /mnt/test/0/2 8 drwxr-xr-x 2 root root 4096 Jun 17 02:20 /mnt/test/0/3 9 drwxr-xr-x 6 root root 4096 Jun 17 02:20 /mnt/test/1/ 10 drwxr-xr-x 2 root root 4096 Jun 17 02:20 /mnt/test/1/0 11 drwxr-xr-x 2 root root 4096 Jun 17 02:20 /mnt/test/1/1 12 drwxr-xr-x 2 root root 4096 Jun 17 02:20 /mnt/test/1/2 13 drwxr-xr-x 2 root root 4096 Jun 17 02:20 /mnt/test/1/3 nilfs2 mkfs.nilfs2 (nilfs-utils 2.1.6) Start writing file system initial data to the device Blocksize:4096 Device:nilfs2.img Device Size:549755813888 File system initialization succeeded !! 12 drwxr-xr-x 6 root root 4096 Jun 17 02:32 /mnt/test/0/ 17 drwxr-xr-x 2 root root 4096 Jun 17 02:32 /mnt/test/0/0 18 drwxr-xr-x 2 root root 4096 Jun 17 02:32 /mnt/test/0/1 19 drwxr-xr-x 2 root root 4096 Jun 17 02:32 /mnt/test/0/2 20 drwxr-xr-x 2 root root 4096 Jun 17 02:32 /mnt/test/0/3 21 drwxr-xr-x 6 root root 4096 Jun 17 02:32 /mnt/test/1/ 22 drwxr-xr-x 2 root root 4096 Jun 17 02:32 /mnt/test/1/0 23 drwxr-xr-x 2 root root 4096 Jun 17 02:32 /mnt/test/1/1 24 drwxr-xr-x 2 root root 4096 Jun 17 02:32 /mnt/test/1/2 25 drwxr-xr-x 2 root root 4096 Jun 17 02:32 /mnt/test/1/3
allocation policy 知らないけどどうもシーケンシャルな様子。log structuredだからそんなもんですな。Btrfsではroot inode番号256なことを使って判別すればいいかなという気がする。あとBtrfsはlink-countしないので全部"1"になってるとこでも判別できる。そうやって見ていくと、directoryのsizeのとこでもFS判別できるね。ソムリエ道は奥が深い。
というか、 root directoryのinode番号で結構わかるね(涙 (btrfs:256, ext4:2, xfs:96(設定によるが), f2fs:3, nilfs2: 2)
まだおまけ
こうなるとどうしても root inode番号見ずにf2fsとnilfs2を区別したい。ということで、0/{0,1,2,3}を作る->一度0/{0,1,2,3}を削除->sync->もう一度0/{0,1,2,3}を作るとしてみた結果
f2fs 4 drwxr-xr-x 6 root root 4096 Jun 17 02:36 /mnt/test/0/ 9 drwxr-xr-x 2 root root 4096 Jun 17 02:36 /mnt/test/0/0 10 drwxr-xr-x 2 root root 4096 Jun 17 02:36 /mnt/test/0/1 11 drwxr-xr-x 2 root root 4096 Jun 17 02:36 /mnt/test/0/2 12 drwxr-xr-x 2 root root 4096 Jun 17 02:36 /mnt/test/0/3 nilfs2 mkfs.nilfs2 (nilfs-utils 2.1.6) Start writing file system initial data to the device Blocksize:4096 Device:nilfs2.img Device Size:549755813888 File system initialization succeeded !! 12 drwxr-xr-x 6 root root 4096 Jun 17 02:36 /mnt/test/0/ 13 drwxr-xr-x 2 root root 4096 Jun 17 02:36 /mnt/test/0/0 14 drwxr-xr-x 2 root root 4096 Jun 17 02:36 /mnt/test/0/1 15 drwxr-xr-x 2 root root 4096 Jun 17 02:36 /mnt/test/0/2 16 drwxr-xr-x 2 root root 4096 Jun 17 02:36 /mnt/test/0/3
inode番号がf2fsでは不連続、nilfs2では連続しているのがわかる。syncしているのがポイントでsyncしないとnilfs2でもうまく連続にはならない。
ねえママ, systemd が SEGV したらどうなるの?
Linux 4.5 から cgroup2 というやつが入りました. こいつはいままでの cgroup とは違って, cgroup の tree がシステム全体で唯1つになり, 様々なファイルの名前も変わっています.
そうすると, いろいろと cgroup を使っている systemd にも変更が必要…というわけでいまの systemd の git HEAD では様々開発されているような感じです.
とは言ってもせっかく Linux 4.5 なのだから systemd-229 でも cgroup2 使いたいというわけでがんばってみたら死んだという話. 基本的に必要になるのは次の patch
もともと cgroup2 の挙動は "__DEVEL__sane_behavior" の FS mount option で有効になっていたので, それを切り替えていく patch で, これだけでなんとなく動いている………のだけど
X のセッションからログアウトすると systemd が死ぬ
[ 106.077141] systemd[1]: segfault at 0 ip (null) sp 00007ffd67a25ea8 error 14 in systemd[559755a77000+16e000]
systemd が死んでしまうといろいろと大変で, 様々なサービスや socket が切れてしまうのでだいたいなにもできない. shutdown もまともに走らない.
でも, SEGV したわりには pid 1 はまだ見えていて, 同じみの attempt to kill init も出ていない.
というわけで, systemd が SEGV したらどうなるのかの話になる:
static void install_crash_handler(void) { static const struct sigaction sa = { .sa_handler = crash, .sa_flags = SA_NODEFER, /* So that we can raise the signal again from the signal handler */ }; int r; /* We ignore the return value here, since, we don't mind if we * cannot set up a crash handler */ r = sigaction_many(&sa, SIGNALS_CRASH_HANDLER, -1); if (r < 0) log_debug_errno(r, "I had trouble setting up the crash handler, ignoring: %m"); }
PID 1として起動された場合, systemd は install_crash_handler() で crash handler をいれる. crash 関数はこんな感じ:
noreturn static void crash(int sig) { struct sigaction sa; pid_t pid; … pid = raw_clone(SIGCHLD, NULL); if (pid < 0) log_emergency_errno(errno, "Caught <%s>, cannot fork for core dump: %m", signal_to_string(sig)); else if (pid == 0) { /* Enable default signal handler for core dump */ sa = (struct sigaction) { .sa_handler = SIG_DFL, }; (void) sigaction(sig, &sa, NULL); /* Don't limit the coredump size */ (void) setrlimit(RLIMIT_CORE, &RLIMIT_MAKE_CONST(RLIM_INFINITY)); /* Just to be sure... */ (void) chdir("/"); /* Raise the signal again */ pid = raw_getpid(); (void) kill(pid, sig); /* raise() would kill the parent */ assert_not_reached("We shouldn't be here..."); _exit(EXIT_FAILURE); } else { siginfo_t status; int r; /* Order things nicely. */ r = wait_for_terminate(pid, &status); if (r < 0) log_emergency_errno(r, "Caught <%s>, waitpid() failed: %m", signal_to_string(sig)); else if (status.si_code != CLD_DUMPED) log_emergency("Caught <%s>, core dump failed (child "PID_FMT", code=%s, status=%i/%s).", signal_to_string(sig), pid, sigchld_code_to_string(status.si_code), status.si_status, strna(status.si_code == CLD_EXITED ? exit_status_to_string(status.si_status, EXIT_STATUS_FULL) : signal_to_string(status.si_status))); else log_emergency("Caught <%s>, dumped core as pid "PID_FMT".", signal_to_string(sig), pid); }
crash handler の中身として core dump まわりでは
- fork する
- 子プロセスは
- rlimit で core dump サイズを無制限にする
- cd / する
- kill(getpid()) で自殺
- 親プロセスは
- 子プロセスが自殺するのを待って
- core dump したよーとログに書く
ということになる.
なんとなくうまく動きそうだけれど, systemd には罠がある.
"/usr/lib/sysctl.d/50-coredump.conf" の設定によって systemctl の "kernel.core_pattern" が書きかえられ, コアダンプは "/usr/lib/systemd/systemd-coredump" にパイプされるようになる. このプログラムは "coredumpctl" コマンド管理下にコアを保存して, コアを圧縮してくれたり, スタックトレースをとってくれたりとなかなかべんりにコアを扱えるコマンドになっている.
UID: 119 (sddm) GID: 984 (sddm) Signal: 11 (SEGV) Timestamp: Fri 2016-04-29 07:53:50 JST (15h ago) Command Line: /usr/bin/sddm-greeter --socket /tmp/sddm-:0-uKcNEp --theme /usr/share/sddm/themes/breeze Executable: /usr/bin/sddm-greeter Control Group: /user.slice/user-119.slice/session-c2.scope Unit: session-c2.scope Slice: user-119.slice Session: c2 Owner UID: 119 (sddm) Boot ID: 9790002963754beba034ca2ef60a1ae9 Machine ID: fa62c26247d1db1dc2be16db53adb1e7 Hostname: ako Coredump: /var/lib/systemd/coredump/core.sddm-greeter.119.9790002963754beba034ca2ef60a1ae9.13748.1461884030000000000000.lz4 Message: Process 13748 (sddm-greeter) of user 119 dumped core. Stack trace of thread 13748: #0 0x00007fc7199bb1b0 _ZN7QSGNode7setFlagENS_4FlagEb (libQt5Quick.so.5) #1 0x00007fc719a14a10 n/a (libQt5Quick.so.5) #2 0x00007fc719a150aa _ZN19QQuickWindowPrivate15updateDirtyNodeEP10QQuickItem (libQt5Quick.so.5) #3 0x00007fc719a15b7b _ZN19QQuickWindowPrivate16updateDirtyNodesEv (libQt5Quick.so.5) #4 0x00007fc719a16c00 _ZN19QQuickWindowPrivate14syncSceneGraphEv (libQt5Quick.so.5) #5 0x00007fc7199e4ff8 n/a (libQt5Quick.so.5) #6 0x00007fc7199e66f8 n/a (libQt5Quick.so.5) #7 0x00007fc718db4af5 _ZN7QWindow5eventEP6QEvent (libQt5Gui.so.5) #8 0x00007fc719a201f5 _ZN12QQuickWindow5eventEP6QEvent (libQt5Quick.so.5) #9 0x00007fc718a6cd5a n/a (libQt5Core.so.5) #10 0x00007fc718a6ce8a _ZN16QCoreApplication15notifyInternal2EP7QObjectP6QEvent (libQt5Core.so.5) #11 0x00007fc718daabcd _ZN22QGuiApplicationPrivate18processExposeEventEPN29QWindowSystemInterfacePrivate11ExposeEventE (libQt5Gui.so.5) #12 0x00007fc718dab80d _ZN22QGuiApplicationPrivate24processWindowSystemEventEPN29QWindowSystemInterfacePrivate17WindowSystemEventE (libQt5Gui.so.5) #13 0x00007fc718d89f0b _ZN22QWindowSystemInterface22sendWindowSystemEventsE6QFlagsIN10QEventLoop17ProcessEventsFlagEE (libQt5Gui.so.5) #14 0x00007fc70f1472c0 n/a (libQt5XcbQpa.so.5) #15 0x00007fc714b09dfd g_main_dispatch (libglib-2.0.so.0) #16 0x00007fc714b0a0e0 g_main_context_iterate (libglib-2.0.so.0) #17 0x00007fc714b0a18c g_main_context_iteration (libglib-2.0.so.0) #18 0x00007fc718abbeef _ZN20QEventDispatcherGlib13processEventsE6QFlagsIN10QEventLoop17ProcessEventsFlagEE (libQt5Core.so.5) #19 0x00007fc718a6bd1a _ZN10QEventLoop4execE6QFlagsINS_17ProcessEventsFlagEE (libQt5Core.so.5) #20 0x00007fc718a7383c _ZN16QCoreApplication4execEv (libQt5Core.so.5) #21 0x0000000000414b04 main (sddm-greeter) #22 0x00007fc717f107c0 __libc_start_main (libc.so.6) #23 0x0000000000414bc9 _start (sddm-greeter) Stack trace of thread 13750: #0 0x00007fc717fcf78d __poll (libc.so.6) #1 0x00007fc719e87ac2 _xcb_conn_wait (libxcb.so.1)
実は systemd-229 ではこの coredumpctl のコア保存部分が rework されている. 以前は "systemd-coredump" がコアの解析から保存までなにからなにまで自分一人でやっていたが, systemd-229 からは "systemd-coredump" は最小限のメタデータ収集などの仕事だけをやって, 得られたデータの保存は "systemd-coredump@.service"というサービスを随時立ち上げて行なうことになった.
これ自体はコアダンプの保存に nice とか IO優先度とかかけられるようになって嬉しい, ことなのだけれど, systemd の SEGV とあわさると事態が最悪になる. systemd が SEGV する -> systemd の crash handler が fork して自殺 -> カーネルにより systemd-coredump が呼びだされる -> systemd-coredump は PID 1につなぎにいこうとするが… -> つながらないのでコアは保存しません!!!!
っということになってしまうのだ. というわけでみんなも systemd が SEGV してるなー?おかしいなー?なんか変だなー?と思った時は "/proc/sys/kernel/core_pattern" をちゃんと書きかえようね, という話になりますね
それと Gentoo では "/usr/lib/sysctl.d/50-coredump.conf.disabled" となっていて, core_pattern の設定が systemd-coredump を指すようになっていない. (これは journal にコアを保存するのはよくないね, などという話があったため) その一方で systemd-229 は問答無用で PID 1 起動時に /proc/sys/kernel/core_pattern に "|/bin/false" を書くようになっているので, Gentoo のように無効化しているとコアが全く出なくなったね, ということになるので気をつけて.
じゃあ systemd-229 はなぜそんなことを…ということなのだけれど, これは systemd-coredump が rlimit 考慮してくれるからデフォルトで rlimit 無制限にしとこう -> でもこれだと systemd-coredump が core_pattern に設定されるまではやばいから "|/bin/false" 書いておこう, ということで一応まあ筋は通っている.
で, その SEGV の原因は? というのも気になることだろうし解説しておこう. cgroup1 ではゾンビ化したプロセスは即座に root group につなぎ変えられたが, cgroup2 では死亡のタイミングではまだ元のグループに残ることになっている. 一方で systemd は「グループが空っぽになったよ」というイベントを待ちうけていて空のグループの削除を行なってもいる. そうすると, ざっくりとは なんかのプロセスがゾンビ化 -> ゾンビのグループを取得・グループが空になったから削除 -> 存在しないグループのハンドラ(NULL)を呼びだすぞーとなって SEGV する. 以下の patch で直ってるよ
ということで, systemd に patch を当ててどんどん cgroup2 を楽しんでいこうな. systemd git HEAD だと昨日のぼくの patch ももう入っててもっとよさそうだぞ.
systemd-cgtop の SEGV を直そう
systemd-cgtop という systemd 環境で CGroup ごとの CPU, Memory, Block IO usage を top のように見ることができるコマンドがあります. こんな感じ:
Control Group Tasks %CPU Memory Input/s Output/s / - 123.4 6.3G - - /system.slice 102 82.9 1.9G - - /system.slice/run-ree3377384dda4871ad4e305532d702d8.scope 50 79.7 1.5G - - /user.slice 578 40.2 3.7G - - /system.slice/sddm.service 3 2.5 286.5M - - /system.slice/NetworkManager.service 7 0.5 9.3M - - /system.slice/dbus.service 1 0.1 26.8M - - /system.slice/avahi-daemon.service 2 0.0 1.1M - - /init.scope 1 - 4.2M - - /system.slice/acpid.service 1 - 264.0K - - /system.slice/boot-efi.mount - - 12.0K - - /system.slice/chronyd.service 1 - 616.0K - - /system.slice/cronie.service 1 - 3.6M - - /system.slice/cups.service 1 - 7.8M - - /system.slice/dev-hugepages.mount - - 16.0K - - /system.slice/dev-mqueue.mount - - 1.3M - - /system.slice/dev-sda6.swap - - 20.0K - - /system.slice/distccd.service 8 - 1.2M - - /system.slice/geoclue.service 4 - 7.8M - - /system.slice/nullmailer.service 1 - 1.9M - - /system.slice/polkit.service 5 - 4.6M - - /system.slice/rtkit-daemon.service 3 - 420.0K - - /system.slice/sshd.service 1 - 1.3M - - /system.slice/sys-kernel-config.mount - - 16.0K - - /system.slice/system-getty.slice 1 - 208.0K - - /system.slice/system-getty.slice/getty@tty2.service 1 - - - - /system.slice/system-systemd\x2dbacklight.slice - - 16.0K - - /system.slice/system-systemd\x2dcoredump.slice - - 416.0K - - /system.slice/system-systemd\x2dfsck.slice - - 48.0K - - /system.slice/systemd-journald.service 1 - 12.8M - - /system.slice/systemd-logind.service 1 - 1.0M - - /system.slice/systemd-udevd.service 1 - 6.0M - - /system.slice/udisks2.service 5 - 2.6M - - /system.slice/upower.service 3 - 2.5M - - /system.slice/wpa_supplicant.service 1 - 1.6M - - /user.slice/user-1000.slice 574 - - - - /user.slice/user-1000.slice/session-1.scope 572 - - - - /user.slice/user-1000.slice/user@1000.service 2 - - - - /user.slice/user-119.slice 4 - - - - /user.slice/user-119.slice/session-c1.scope 2 - - - - /user.slice/user-119.slice/user@119.service 2 - - - -
ところが, これがうちの環境で SEGV していたので追っかけた話です.
naota ~ # gdb systemd-cgtop (gdb) r Starting program: /usr/bin/systemd-cgtop [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1". Program received signal SIGSEGV, Segmentation fault. refresh_one (controller=<optimized out>, path=0x5555557ac240 "", a=0x5555557ac270, b=0x5555557ac2a0, iteration=0, depth=0, ret=0x0) at /var/tmp/portage/sys-apps/systemd-229/work/systemd-229/src/cgtop/cgtop.c:407 407 child && (gdb) bt #0 refresh_one (controller=<optimized out>, path=0x5555557ac240 "", a=0x5555557ac270, b=0x5555557ac2a0, iteration=0, depth=0, ret=0x0) at /var/tmp/portage/sys-apps/systemd-229/work/systemd-229/src/cgtop/cgtop.c:407 #1 0x000055555555a4d3 in refresh (iteration=<optimized out>, b=0x5555557ac2a0, a=0x5555557ac270, root=0x5555557ac240 "") at /var/tmp/portage/sys-apps/systemd-229/work/systemd-229/src/cgtop/cgtop.c:442 #2 main (argc=<optimized out>, argv=<optimized out>) at /var/tmp/portage/sys-apps/systemd-229/work/systemd-229/src/cgtop/cgtop.c:936 (gdb) p child $1 = (Group *) 0x7fff00000020 (gdb) list 402 if (r < 0) 403 return r; 404 405 if (arg_recursive && 406 IN_SET(arg_count, COUNT_ALL_PROCESSES, COUNT_USERSPACE_PROCESSES) && 407 child && 408 child->n_tasks_valid && 409 streq(controller, SYSTEMD_CGROUP_CONTROLLER)) { 410 411 /* Recursively sum up processes */
gdb で動かしてみるとやっぱり死ぬので backtrace とソース見たところ. child あたりがあやしいので見ていく
(gdb) p child $2 = (Group *) 0x7fff00000020 (gdb) p child->n_tasks_valid Cannot access memory at address 0x7fff00000020
ということで, child に変な値入ってるっぽい. これどうせ初期化忘れだろうなという気持ちで見ていく.
ちょっと上を見ると
r = refresh_one(controller, p, a, b, iteration, depth + 1, &child);
child はここで設定されてそう. refresh_one() を見ていく
static int refresh_one( const char *controller, const char *path, Hashmap *a, Hashmap *b, unsigned iteration, unsigned depth, Group **ret) { _cleanup_closedir_ DIR *d = NULL; Group *ours; int r; assert(controller); assert(path); assert(a); if (depth > arg_depth) return 0; r = process(controller, path, a, b, iteration, &ours); if (r < 0) return r; … if (ret) *ret = ours; return 1; }
"*ret = ours" されてて, "process(controller, path, a, b, iteration, &ours);" されてるけど, ours が初期化されていない. ただ "if (r < 0)" でエラー処理はされてて, その場合 ret が更新されないのが気になる.
process() を見てみると, どうもエラーが起きているのに 0 を返しているパスがある. この場合, ours は変更されない.
} else if (streq(controller, "blkio") && cg_unified() <= 0) { _cleanup_fclose_ FILE *f = NULL; _cleanup_free_ char *p = NULL; uint64_t wr = 0, rd = 0; nsec_t timestamp; r = cg_get_path(controller, path, "blkio.io_service_bytes", &p); if (r < 0) return r; f = fopen(p, "re"); if (!f) { if (errno == ENOENT) return 0; return -errno; }
ここで2つの論点がある. 1つはこの 0 を返しているのはどういうパスか, もう1つは ours をどこで NULL にするのが適切であるか.
前記のコードは "blkio.io_service_bytes" というファイルを開いている. これは cgroup の blkio tree で cgroup ごとの IO 量の統計を取得するファイルである. このファイルはIOスケジューラが cfq でなければ存在しない. そのため cfq をカーネルに組み込んでいないシステムでは ours が変更されずに process() から返っていくということになる.
- process() の先頭で NULL にする
- process() の return 部分で NULL にする
- refresh_one() で NULL 初期化する
systemd ではエラーの場合, "call-by-reference variables" を書きかえるな, という Coding Style なので*1, 1番はよろしくない.
2番は変更が面倒だけど, 正しい動作をする. 3番はシンプルだし, ここではうまく動くのだが全体的にはよくないところがある.
そもそも process() は refresh_one() 以外からも呼ばれうる. そして呼び出し側は refresh_one() が 0 を返してきた時, 返ってきたものが NULL か, 正しい Group を指していることを期待している. そうすると, 3番の修正だけでは不十分だということが言える.
って, 最後のはこれを書いてて気がついたんだけど, やっぱり面倒だしもう3番で PR 投げてしまったし, とりあえずね, 放置.
Software Design 2016年05月号: 仮想マシン用の準仮想化ドライバデバイスドライバのフレームワークvirtioドライバのしくみ
先月紹介記事を書いてから、他の記事も書こうかなー、wineのlocale話しでも書こうかなーと思っていたんですが、そのうちに次の雑誌が来ました。間に合わなかったよ…
今月号で50回目です. virtioドライバがvirtio-pciでdetectされ、読みこまれ、ringで通信する様子などを書いています.
その他にも, 今月のSoftware DesignにはなんとSIMカードもついていたので、後ほどよく読んでSIM登録手続きしちゃおかなという感じです.
そのうちwineのlocale話しとか, いまsystemdがSEGVしている話しも書きたい.
Software Design 2016年04月号: Linux 4.1から4.4までのeBPFに関する開発
Linux 4.3のIO/FSまわりcommit
今日読んだ分をざっくりとtweetから持ってきた
“Btrfs: add support for blkio controllers” / “kernel/git/torvalds/linux.git - Linux kernel source tree” http://t.co/w1lWm62WMD
— キーリ (@naota344) 2015, 9月 7
Btrfsは独自にbioをsubmitしているので、blkio controllerにacountingされていなかったのを直したような感じ。複数disk対応にはまだ作業が必要とか
これ踏んでたのかなあ…“Btrfs: check if previous transaction aborted to avoid fs corruption” / “kernel/git/torvalds/linux.git -…” http://t.co/OYmyYjaue8
— キーリ (@naota344) 2015, 9月 7
transactionが別のtransactionの完了を待っていたが、正常終了かどうかをcheckしていなかった。そのためcommitされていないerror transactionに依存する形でtransactionがcommitされてたという問題のfix
btrfs, empty chunkのauto cleanupが入ったのか。あいてるように見えるのに書けないの問題が少し解消するかな
— キーリ (@naota344) 2015, 9月 7
Btrfsではdfで空きスペースがあるのにfileが作れない消せないということがあって問題とされていた. 原因の1つとして、空になったchunkを回収してない -> 新しいchunkをallocできないというのがあった. いつのまにか空のchunkを回収するコードが入っていたらしい。
“inode: don't softlockup when evicting inodes” / “kernel/git/torvalds/linux.git - Linux kernel source tree” http://t.co/oH5ck1Wztx
— キーリ (@naota344) 2015, 9月 7
148GB RAMなマシンで、inodeをメモリから消すほどメモリがいっぱいにならない
なので、数億のempty fileを作ってumountすると数億分のevict inodeが起きてsoft lockupするbugのfix
https://t.co/bUSBn2qU4N
— キーリ (@naota344) 2015, 9月 7
これはそのまま。evict inodeに時間がかかるほど大量のinodeを溜め込めるメモリがあるマシンじゃないといけないけど、そういうこともあるのかーと面白かった。
“sync: serialise per-superblock sync operations” / “kernel/git/torvalds/linux.git - Linux kernel source tree” http://t.co/Uop8AmRVeS
— キーリ (@naota344) 2015, 9月 7
複数のsyncが同時に同じFSにかかると、inode listのspin lockがcontentionし、無駄にCPUを使ってしまう。
mutex lockをとるようにして、syncをserialiseした
https://t.co/g4KJa0hut1
— キーリ (@naota344) 2015, 9月 7
syncで無駄にCPUを使うことがなくなってよかったというはなし。ついついsyncしちゃう人に朗報?(謎
“writeback: plug writeback at a high level” / “kernel/git/torvalds/linux.git - Linux kernel source tree” http://t.co/bfUtYrS7As
— キーリ (@naota344) 2015, 9月 7
これまでper-mappingでIOをまとめていた。複数fileに同時にwriteし、disk上でその2つのfileのblockが連続でも、2つのIOはまとめられない。writebackのplugを行ないIOがまとまるようにした
https://t.co/DJxlPV1Ap3
— キーリ (@naota344) 2015, 9月 8
さっきのcommit, benchmarkがよくて,大量の4KB fileを作るテストだけれど95%のIOPS削減(それだけ大きなIOにまとめられた)し、bandwidthが38%改善している。実行時間も26%減少。
— キーリ (@naota344) 2015, 9月 8
benchがSSDで行なわれているので、HDDではもっとbenefitが出るだろう。
— キーリ (@naota344) 2015, 9月 8
複数fileに同時にIO operation(しかもdisk上では並んでいる & fsyncはしてない)というのは実際どういうオペレーションになるのか少し不明だけどSSDでもこれだけパフォーマンス改善するのは面白い
GoogleのKernelThreadSanitizerが、swaponのpotential data raceを発見
https://t.co/xxedbvj3e3
— キーリ (@naota344) 2015, 9月 8
KTSan面白そうなので触ってみたい
おっxfsのcorruption情報だ(log recoveryのsizeに不正なものを入れとくやつ)“xfs: validate transaction header length on log recovery” / “ker…” http://t.co/i9xxbsduwP
— キーリ (@naota344) 2015, 9月 8
Linux 4.3結構xfs log recoveryのとこにfix入ってるな。log がcorruptした時にそのまま進んでしまわないようなcheckが多く導入されている感じ。
— キーリ (@naota344) 2015, 9月 8
log部分のchecksumとかsizeとか、logのcancelが正しく動くかなどlogまわりに多くのfixが入っている
“xfs: Fix file type directory corruption for btree directories” / “kernel/git/torvalds/linux.git - Linux kernel sou…” http://t.co/sZkJKN3IDb
— キーリ (@naota344) 2015, 9月 8
ディレクトリエントリ置換前のfiletypeと置換後のfiletypeをうっかり同じ変数に保存していたため, filetypeがおかしくなってたよというバグ. これは怖い…