目次
カーネルビルド
takayuki@ubuntu3:~/repos$ sudo apt update
takayuki@ubuntu3:~/repos$ sudo apt install build-essential make ninja-build meson pkg-config autoconf kernel-package libncurses5-dev bison flex libssl-dev fio python2 libelf-dev rsync zstd udev
takayuki@ubuntu3:~/repos$ git clone https://github.com/snu-csl/rfuse
takayuki@ubuntu3:~/repos/rfuse/linux$ sudo make menuconfig
.configを修正。
CONFIG_MODULE_SIG_KEY="certs/signing_key.pem"
#CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"
CONFIG_SYSTEM_TRUSTED_KEYS=""
#CONFIG_SYSTEM_REVOCATION_KEYS="debian/canonical-revoked-certs.pem"
CONFIG_SYSTEM_REVOCATION_KEYS=""
CONFIG_MODULE_SIG_KEYは””とした場合、”sign-file: : No such file or directory”エラーがでたため、”certs/signing_key.pem”を設定した。
takayuki@ubuntu3:~/repos/rfuse/linux$ sudo make-kpkg -j 4 --initrd --revision=1.0 kernel_image kernel_headers
takayuki@ubuntu3:~/repos/rfuse/linux$ cd ../
takayuki@ubuntu3:~/repos/rfuse$ sudo dpkg -i *.deb
takayuki@ubuntu3:~$ grep -e menuentry -e submeny /boot/grub/grub.cfg
if [ x"${feature_menuentry_id}" = xy ]; then
menuentry_id_option="--id"
menuentry_id_option=""
export menuentry_id_option
menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-529721a0-6f53-4125-8f3e-fd5dc5f673f5' {
submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-529721a0-6f53-4125-8f3e-fd5dc5f673f5' {
menuentry 'Ubuntu, with Linux 5.15.0-107-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.15.0-107-generic-advanced-529721a0-6f53-4125-8f3e-fd5dc5f673f5' {
menuentry 'Ubuntu, with Linux 5.15.0-107-generic (recovery mode)' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.15.0-107-generic-recovery-529721a0-6f53-4125-8f3e-fd5dc5f673f5' {
menuentry 'Ubuntu, with Linux 5.15.0-97-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.15.0-97-generic-advanced-529721a0-6f53-4125-8f3e-fd5dc5f673f5' {
menuentry 'Ubuntu, with Linux 5.15.0-97-generic (recovery mode)' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.15.0-97-generic-recovery-529721a0-6f53-4125-8f3e-fd5dc5f673f5' {
1>6★menuentry 'Ubuntu, with Linux 5.15.0' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.15.0-advanced-529721a0-6f53-4125-8f3e-fd5dc5f673f5' {
menuentry 'Ubuntu, with Linux 5.15.0 (recovery mode)' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.15.0-recovery-529721a0-6f53-4125-8f3e-fd5dc5f673f5' {
menuentry 'Memory test (memtest86+)' {
menuentry 'Memory test (memtest86+, serial console 115200)' {
vi /etc/default/grub
GRUB_DEFAULT="1>6"
#GRUB_TIMEOUT_STYLE=hidden
GRUB_TIMEOUT=5
update-grub2
rqueue数を設定
driver/rfuse/rfuse.hとlib/librfuse/include/rfuse.h両方のRFUSE_NUM_IQUEUEをコア数に合わす。
# (librfuse)
$ cd lib/librfuse
$ vi include/rfuse.h
# (rfuse kernel driver)
$ cd driver/rfuse
$ vi rfuse.h
--> Change the value of RFUSE_NUM_IQUEUE to the number of core in machine.
libfuseインストール
takayuki@ubuntu3:~/repos/rfuse/lib/librfuse$ sudo bash ./librfuse_install.sh
rfuseドライバインストール
takayuki@ubuntu3:~/repos/rfuse$ cd driver/rfuse
takayuki@ubuntu3:~/repos/rfuse/driver/rfuse$ sudo make
takayuki@ubuntu3:~/repos/rfuse/driver/rfuse$ sudo ./rfuse_insmod.sh first
# Ubuntu (Add below line into .bashrc for system-wide adoption).
$ export LD_LIBRARY_PATH=/usr/local/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}
StackFSを試す
takayuki@ubuntu3:~$ sudo modprobe -r brd
takayuki@ubuntu3:~$ sudo modprobe brd rd_nr=1 rd_size=$((1024*1024))
takayuki@ubuntu3:~/repos/rfuse$ cd filesystems/stackfs
takayuki@ubuntu3:~/repos/rfuse/filesystems/stackfs$ vi
.gitignore run.sh StackFS_LowLevel.c
Makefile StackFS_ll
takayuki@ubuntu3:~/repos/rfuse/filesystems/stackfs$ vi run.sh
takayuki@ubuntu3:~/repos/rfuse/filesystems/stackfs$ ./run.sh ssd
DEVICE_NAME="/dev/ram0" ★
性能測定
fio -filename=/mnt/test/test1m -rw=write --time_based --runtime=60 -bs=4k -size=1M -numjobs=1 -fallocate=none -group_reporting -name=file1 --numjobs=4 --iodepth=16 --direct=1
Rfuse read: 2497MB/s
Rfuse write: 520MB/s
Fuse read: 1177MB/s
Fuse write: 771MB/s
RAM read: 3078MB/s
RAM write: 3164MB/s
file競合が大きくなりすぎたので、アクセス先を4ファイルに分ける。
fio -directory=/mnt/test -rw=write --time_based --runtime=60 -bs=1M -size=1M -numjobs=1 -fallocate=none -group_reporting -name=file1 --numjobs=4 --iodepth=16
fio -directory=/mnt/test -rw=read --time_based --runtime=60 -bs=1M -size=1M -numjobs=1 -fallocate=none -group_reporting -name=file1 --numjobs=4 --iodepth=16
rfuse_read(4 file): 1485MB/s
rfuse_write(4 file): 867MB/s
性能問題
ユーザ空間にrequestを出したあと、rfuse_completion_pollで応答待ちをする。そのたびにscheduleに入る。contextスイッチが多発する。
root@ubuntu3:/home/takayuki/repos/linux/tools/perf# ./perf record -F 99 -a -g — sleep 30
root@ubuntu3:/home/takayuki/flamegraph# ./perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > ram_read_nosche_perf.svg
・rfuseではユーザ空間デーモンの処理完了後に、カーネルは共有メモリ上のフラグを調べて処理完了を検知する。
・共有メモリのフラグはrfuse_completion_pollでmax_idle_due(デフォルト50us)ポーリングする。ただし、loopの際に別スレッド実行(schedule())を実行するため、コンテキストスイッチが多発する。
#define RFUSE_COMP_MAX_IDLE 50 // Maximum idle time of completion thread
int rfuse_completion_poll(struct fuse_conn *fc, struct rfuse_iqueue *riq, struct rfuse_req *r_req)
{
unsigned long max_idle_due = jiffies + usecs_to_jiffies(RFUSE_COMP_MAX_IDLE);
while(fc->connected) {
if(test_bit(FR_FINISHED, &r_req->flags)){★フラグ
rfuse_request_end(r_req);
return 0;
}
if(time_after(jiffies, max_idle_due)){
rfuse_sleep_comp(fc, riq, r_req);
}
schedule(); ★★ここが思老い。
}
return -ENOTCONN;
}
RFUSEではring bufferをユーザ空間プロセスとカーネルが共有し、それぞれの書き込みをお互いに監視する仕組みである。ユーザ空間プロセスはFUSEのようにsystemcallを発行する仕組みではない。処理完了のタイミングをカーネルが知ることができない。
RFUSEではその対策としてカーネルスレッドが一定期間(50us = max_idle_due)ポーリングし、scheduleに入る処理を行う。この仕組みをHybrid pollingと呼ぶ。
As a solution, RFUSE adopts a hybrid polling approach.
There is a user-defined period (50µsec, by default) during
which a thread can perform busy-waiting idly. If the application thread in the polling state exceeds this period, it will
enter the sleep state, waiting for the completion flag to be
set. For requests that can be quickly handled by the userspace
implementation, the application thread can receive a reply
during polling and return promptly. Otherwise, for requests
with longer latency, it will enter the sleep state, thus avoiding
unnecessary CPU wastage.
現在の実装ではmax_idle_dueに到達する前に他スレッドに切り替わってしまうため、コンテキストスイッチオーバヘッドが大きくなりすぎてしまう。scheduleについては論文内に記述がない。ここでは外した方がいい?
→sheduleをコメントアウトしても、1コアがrfuse_completion_pollに占有される。2コアしかないので性能がでない。
6コアセイノウ
SeqRD: 1769MiB/s
SeqWR:1584MiB/s
RAM read: 3460MiB/s
RAM write: 2157MB/s
※サンプルプログラムの実装上RFUSEの性能が落ちるのはしょうがない。
RAM: fio → (syscall) → device
RFUSEは: fio → (syscall) → RFUSEドライバ → RFUSEデーモン → (syscall) → device
RFUSEを6コアで6JOBで動かした場合、fioが6プロセスコアまたがりで動作する。
①fioからのwrite systemコール→②エンキュー→③ユーザ空間プロセス(StackFS)が読み取り→④RAMディスクへのIO、⑤カーネルへの応答、⑥カーネル内の応答待ちポーリング、が全て同一コアで動く。
→ ②と⑥の間でコンテキストスイッチが必ず発生。
→ 3JOBだと問題ない?
※問題の本質
→ Ring bufferをカーネル空間で実装する場合、カーネル空間でポーリングが必要になる。rfuseドライバでポーリングし続けた場合、CPUを使用し続けてしまう。それをふせぐためにscheduleをよんで別の処理を実施している。これはCPUを占有してしまい他の処理を完全にブロックしてしまうとシステム停止するため必要。
→
→ポーリング要否をschedulerに入れられないか?
問題を整理
ユーザ空間ファイルシステムやブロックデバイスでは、カーネル空間からユーザ空間にIOを転送する必要がある。
カーネル空間/ユーザ空間の通信でSyscall時のコンテキストスイッチのオーバヘッドやメモリコピーのオーバヘッドがある。
RFUSEはSyscallやメモリコピーを省いた。ただしsystcallの代わりにio_uring形式のring bufferでsyscallを失くした。その結果、ユーザ空間とカーネル空間の両方でポーリングが必要となった。
カーネル空間でポーリングした場合CPUを占有した場合、他の処理のCPU使用時間が減る。どこかで他の処理にCPUを明け渡す必要がある。RFUSEではHybrid pollingという仕組みで、一定期間busy waitしたら他のschedule()を呼出し他の処理を実行している。
RFUSEに課題
① Hybridポーリングは一定時間ポーリングをしたあとポーリングを中断するため、低負荷時にレイテンシが低下する。
② コアがアプリの並列度とカーネルポーリングの並列度の和より大きくなった場合に、ポーリング処理同士が干渉(コンテキストスイッチが発生)し、性能が劣化する。
→ポーリングの制御が必要。
デバッグ
root@ubuntu3:/home/takayuki/repos/rfuse/filesystems/stackfs# echo /tmp/core > /proc/sys/kernel/core_pattern
root@ubuntu3:/home/takayuki/repos/rfuse/filesystems/stackfs# gdb --args ./StackFS_ll -r /mnt/RFUSE_EXT4 /mnt/test
トラブルシュート
gvfsがfuseを使っているのでfuseを入れ替えられない。
GVFS_DISABLE_FUSE=1を設定すればfuseを使わなくなる。
root@ubuntu3:/home/takayuki# sudo vi /etc/environment
GVFS_DISABLE_FUSE=1