すっかり遅くなってしまいすみません、Linux Advent Calendar 2020 20日目の記事です。
今日はLinux-5.9.3で提供されたfsopen(2)を試してみた(けどサンプルを動かすとこまでたどり着けなかった...)という話をしようと思います。
背景
個人的に気が向いた時にThe Linux Kernel Archivesにアクセスし、Linuxカーネルの新バージョンがリリースされていたら手元でビルドしてみる、という趣味(?)があるのですが、Linux-5.9.3のソースコードにてfsopen(2)というシステムコールを追加したというコミットログを見かけました。
fsopen(2)...?聞いたことのないシステムコールだな...と思ったのでちょっと試してみたというのが今日のお話になります。
fsopen(2)とは?
fsopen(2)は新しいシステムコールのようで、ブロックデバイスを渡すことでプロセス実行時に動的にファイルシステムをmountするための機能に見えます(具体的なユースケースが個人的には明確化できていないです...)。
fsopen(2)は linux-5.9.3fs/fsopen.c
で定義されています。引数 _fs_name
で指定したファイルシステムへの操作用ファイルディスクリプタを取得するような動作に見えます。
108 /*
109 * Open a filesystem by name so that it can be configured for mounting.
110 *
111 * We are allowed to specify a container in which the filesystem will be
112 * opened, thereby indicating which namespaces will be used (notably, which
113 * network namespace will be used for network filesystems).
114 */
115 SYSCALL_DEFINE2(fsopen, const char __user *, _fs_name, unsigned int, flags)
116 {
...
128 fs_name = strndup_user(_fs_name, PAGE_SIZE);
129 if (IS_ERR(fs_name))
130 return PTR_ERR(fs_name);
131
132 fs_type = get_fs_type(fs_name);
133 kfree(fs_name);
134 if (!fs_type)
135 return -ENODEV;
136
137 fc = fs_context_for_mount(fs_type, 0);
...
148 return fscontext_create_fd(fc, flags & FSOPEN_CLOEXEC ? O_CLOEXEC : 0);
引数 _fs_name
に指定する値はfsopen(2)のコミットメッセージにサンプルの形で記載されています。以下はコミットメッセージからの引用です。 ext4
とか afs
といったファイルシステム名をそのまま指定すれば良さそうです。
For example:
sfd = fsopen("ext4", FSOPEN_CLOEXEC);
...
sfd = fsopen("afs", -1);
さらにコミットメッセージをみると、fsopen(2)で取得したファイルディスクリプタに対して fsconfig(2)
を呼び出すことで必要なオプションを設定して行くようです。そして最後に fsmount(2)
move_mount(2)
を呼ぶことで実行時にファイルシステムをmountするような挙動に見えます。
sfd = fsopen("ext4", FSOPEN_CLOEXEC);
fsconfig(sfd, FSCONFIG_SET_PATH, "source", "/dev/sda1", AT_FDCWD);
fsconfig(sfd, FSCONFIG_SET_FLAG, "noatime", NULL, 0);
fsconfig(sfd, FSCONFIG_SET_FLAG, "acl", NULL, 0);
fsconfig(sfd, FSCONFIG_SET_FLAG, "user_xattr", NULL, 0);
fsconfig(sfd, FSCONFIG_SET_STRING, "sb", "1", 0);
fsconfig(sfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
fsinfo(sfd, NULL, ...); // query new superblock attributes
mfd = fsmount(sfd, FSMOUNT_CLOEXEC, MS_RELATIME);
move_mount(mfd, "", sfd, AT_FDCWD, "/mnt", MOVE_MOUNT_F_EMPTY_PATH);
サンプルを例にユーザランド側からfsopen(2)を呼び出してみる
Linux-5.9.3/samples/vfs/test-fsmount.c
には fsopen(2)
を使用するサンプルが用意されています。これを使用して実際に fsopen(2)
の挙動を見てみましょう。
(が、後述しますが 現時点ではサンプルをうまく動かすことができませんでした ...(T_T))
サンプルをビルドする際のハマり所
Linux-5.9.3/samples/vfs/test-fsmount.c
と fsopen(2)のコミットメッセージ内のサンプルコードを比べると、オプション名が微妙に異なったりしているケースがあるようです。実際にサンプルを動かす際には、ソースコードと照らし合わせながら、現状の定義(マクロ定数とか)に読み替える必要がありそうです。
また、馴染み深い(?) open(2)
のようなシステムコールは、ユーザランド向けのシステムコール呼び出し定義が用意されていますが、 Linux-5.9.3
をビルドしてカーネルをインストールするケースにおいては、この部分を自前でなんとかする形になります。
Linux-5.9.3/samples/vfs/test-fsmount.c
を見ると以下のような定義があり、 __NR_fsopen
にシステムコール番号を定義しておき、 syscall()
でシステムコールを直接呼び出すという実装になっています。さらに、これらの値は -1
になっており、ソースコードコメントの"Hope -1 isn't a syscall"から察するに、 fsopen(2)
などのシステムコール番号をちゃんと設定しないとダメそうです...。
/* Hope -1 isn't a syscall */
#ifndef __NR_fsopen
#define __NR_fsopen -1
#endif
#ifndef __NR_fsmount
#define __NR_fsmount -1
#endif
#ifndef __NR_fsconfig
#define __NR_fsconfig -1
#endif
#ifndef __NR_move_mount
#define __NR_move_mount -1
#endif
...
static inline int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}
システムコール番号を把握する
カーネルソースコード内で __NR_fsopen
を検索してみると、以下のようなマクロ定数が見つかります。どうやらこの値をサンプルプログラム側で使用すれば良さそうです。
$ find arch/x86 -type f | grep \\.h | xargs egrep '__NR_fsopen|__NR_fsmount|__NR_fsconfig|__NR_move_mount' | grep unistd_64
arch/x86/include/generated/uapi/asm/unistd_64.h:#define __NR_move_mount 429
arch/x86/include/generated/uapi/asm/unistd_64.h:#define __NR_fsopen 430
arch/x86/include/generated/uapi/asm/unistd_64.h:#define __NR_fsconfig 431
arch/x86/include/generated/uapi/asm/unistd_64.h:#define __NR_fsmount 432
必要なヘッダファイルの用意
サンプルファイルは他にもヘッダファイルを参照しており、Linux環境にインストールされているヘッダファイルではなく、 Linux-5.9.3
に含まれているヘッダファイルを使用する必要があります。
ここでは以下の手順で、サンプルプログラムと同じ場所に include/linux
と uapi
ディレクトリを作成し、そこに必要なヘッダファイルを配置しました。
$ cd /usr/src/samples/vfs/
$ mkdir -p include/linux
$ cp -r ../../include/linux/ ./include/
$ cp -r ../../include/uapi/ ./uapi
サンプルのビルド
以下の手順でサンプルプログラムをコンパイルできます(ただしコンパイル時にwarningが出ます...)。
$ gcc -o test-fsmount test-fsmount.c -I./uapi -I./include/
サンプル用のext4ファイルシステムの作成
サンプルプログラム内の fsopen(2)
で参照するファイルシステムとして、 /dev/sdb
上にext4のファイルシステムを作成しておきます。
$ dmesg | grep sdb
[ 4.375191] sd 3:0:0:0: [sdb] 30714 512-byte logical blocks: (15.7 MB/15.0 MiB)
[ 4.375750] sd 3:0:0:0: [sdb] Write Protect is off
[ 4.376263] sd 3:0:0:0: [sdb] Mode Sense: 00 3a 00 00
[ 4.376284] sd 3:0:0:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[ 4.385157] sd 3:0:0:0: [sdb] Attached SCSI disk
$ sudo mkfs.ext4 /dev/sdb
$ sudo mount /dev/sdb /mnt
$ df -h | egrep 'ファイル|mnt'
ファイルシス サイズ 使用 残り 使用% マウント位置
/dev/sdb 14M 252K 13M 2% /mnt
$
$ sudo umount /mnt
サンプルプログラムを実行してみる
プログラムを実行する前に、サンプルコードをざっと見てみます。 fsopen(2)
でファイルディスクリプタを取得し、 E_fsconfig
(関数内で fsconfig(2)
を呼んでいる)でファイルシステムのパラメータ設定、 fsmount(2)
と move_mount(2)
でファイルシステムのmountという流れになっています。
#define __NR_fsopen 430
#define __NR_fsmount 432
#define __NR_fsconfig 431
#define __NR_move_mount 429
...
int main(int argc, char *argv[])
{
int fsfd, mfd;
/* Mount a publically available AFS filesystem */
fsfd = fsopen("ext4", FSOPEN_CLOEXEC);
if (fsfd == -1) {
perror("fsopen");
exit(1);
}
E_fsconfig(fsfd, FSCONFIG_SET_STRING, "source", "/dev/sdb", 0);
E_fsconfig(fsfd, FSCONFIG_SET_FLAG, "noatime", NULL, 0);
E_fsconfig(fsfd, FSCONFIG_SET_FLAG, "acl", NULL, 0);
E_fsconfig(fsfd, FSCONFIG_SET_FLAG, "user_xattr", NULL, 0);
E_fsconfig(fsfd, FSCONFIG_SET_STRING, "sb", "1", 0);
mfd = fsmount(fsfd, FSMOUNT_CLOEXEC, MOUNT_ATTR_RDONLY|MOUNT_ATTR_NOATIME);
if (mfd < 0)
mount_error(fsfd, "fsmount");
E(close(fsfd));
if (move_mount(mfd, "", AT_FDCWD, "/mnt", MOVE_MOUNT_F_EMPTY_PATH) < 0) {
perror("move_mount");
exit(1);
}
...
が、挙動を調べるためにサンプルプログラムを実行すると"Invalid argument"で fsmount(2)
が失敗してしまいます...。
$ ./test-fsmount
fsmount: Invalid argument
fsmount(2)のエラー個所を探す
エラーメッセージは"Invalid argument"なので、システムコール的には EINVAL
を返しているはずです。 fsmount(2)
のソースコードを参照しながら、エラーの原因を探ってゆきましょう。
/*
* Create a kernel mount representation for a new, prepared superblock
* (specified by fs_fd) and attach to an open_tree-like file descriptor.
*/
SYSCALL_DEFINE3(fsmount, int, fs_fd, unsigned int, flags,
unsigned int, attr_flags)
{
...
if ((flags & ~(FSMOUNT_CLOEXEC)) != 0)
return -EINVAL;
if (attr_flags & ~(MOUNT_ATTR_RDONLY |
MOUNT_ATTR_NOSUID |
MOUNT_ATTR_NODEV |
MOUNT_ATTR_NOEXEC |
MOUNT_ATTR__ATIME |
MOUNT_ATTR_NODIRATIME))
return -EINVAL;
...
switch (attr_flags & MOUNT_ATTR__ATIME) {
case MOUNT_ATTR_STRICTATIME:
printk(KERN_WARNING "--> MOUNT_ATTR_STRICTATIME\n");
break;
case MOUNT_ATTR_NOATIME:
printk(KERN_WARNING "--> MOUNT_ATTR_NOATIME\n");
mnt_flags |= MNT_NOATIME;
break;
case MOUNT_ATTR_RELATIME:
printk(KERN_WARNING "--> MOUNT_ATTR_RELATIME\n");
mnt_flags |= MNT_RELATIME;
break;
default:
printk(KERN_WARNING "--> default\n");
return -EINVAL;
}
...
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)
goto err_fsfd;
...
/* There must be a valid superblock or we can't mount it */
ret = -EINVAL;
if (!fc->root)
goto err_unlock;
...
err_path:
path_put(&newmount);
err_unlock:
mutex_unlock(&fc->uapi_mutex);
err_fsfd:
fdput(f);
return ret;
}
EINVAL
を返す可能性のある個所はいくつかあるようです。順に見てゆくと、まず、引数について指定可能な値以外が渡されてきた場合に EINVAL
になるようです。この点については、有効な引数のみをサンプルプログラム内で指定し、この部分ではエラーにならないようにしています。
printk()
で通過した個所を確認しながら調べてみると、以下の if (!fc->root)
に引っかかることで EINVAL
が返っていることが分かりました。 fc->root
は f.file->private_data->root
を参照しており、ソースコードコードコメントを見るにファイルシステムのsuper blockを指しているようです。
fc = f.file->private_data;
...
/* There must be a valid superblock or we can't mount it */
ret = -EINVAL;
if (!fc->root)
goto err_unlock;
...と、ここまでは把握できたのですが、 fsopen(2)
で渡している /dev/sdb
は mkfs.ext4
でファイルシステムを作ってあるため、super blockが見つけれないのも解せないところです...。
今回はこれ以上の調査はできなかったため、とりあえずサンプルプログラムをビルドするところまでで、 EINVAL
が発生する原因については後ほど調査する感じになりそうです。
まとめ
Linux-5.9.3で提供されているfsopen(2)というシステムコールを試してみました。サンプルプログラムは用意されていますが、現状の実装に合わせて読み替え・修正が必要となっています。また、サンプルプログラム自体をうまく動かせていないため、引き続き調査が必要そうです。