目次
概要
- RFUSEは、FUSEの性能オーバーヘッドを減らし、スケーラブルなカーネル-ユーザ空間通信を実現する新しいユーザ空間ファイルシステムフレームワーク。
- カーネル内のファイルシステムと同等の性能を目指し、高性能デバイス上で優れたパフォーマンスを発揮する。
- 既存のFUSEベースのファイルシステムとの互換性を維持。
背景
- FUSEは、多くのファイルシステムの実装を可能にするが、性能オーバーヘッドが課題。
- ユーザ空間とカーネル空間の通信が性能低下の主因。
- 高性能デバイスの普及に伴い、効率的な通信方法が求められる。
従来技術
- FUSEはユーザ空間でファイルシステムを実装するための標準的なフレームワーク。
- 性能の限界は、カーネル-ユーザ空間間の通信のオーバーヘッドに起因。
- 他の改善案も存在するが、互換性維持と性能向上の両立が課題。
課題
- カーネルとユーザ空間間の通信オーバーヘッドが大きい。
- コンテキストスイッチとリクエストコピーが性能を低下させる。
解決方式
- スケーラブル通信: RFUSEは、カーネルとユーザ空間間の通信にスケーラブルな方法を採用。
- io_uringライクなリングバッファ実装。カーネル→ユーザ空間の通信と改変のしやすさの理由からio_uringではなく独自実装を採用。
- リングバッファ: 各コアごとにリングバッファを使用し、コンテキストスイッチとリクエストコピーを最小限に抑える。
- 互換性維持: 既存のFUSEベースのファイルシステムと互換性を保ちながら、性能を向上。
効果
- 性能向上: 高性能デバイス上でカーネル内ファイルシステムと同等の性能を実現。
- 効率化: カーネル-ユーザ空間間の通信オーバーヘッドを削減。
- 互換性: 既存のFUSEベースのファイルシステムとの互換性を維持。
※EXTFSはExtFUSE(eBPFベースFUSE)。
この研究は、ユーザ空間ファイルシステムの性能向上に大きく貢献し、高性能デバイス上での効率的なファイルシステム運用を可能にする。
実装
rfuse/driver/rfuse at master · snu-csl/rfuse · GitHub
githubに公開されている。
RFUSEドライバ/RFUSEデーモンの通信
- FUSEはFUSEデーモンのread syscallの延長でFUSEドライバがFUSE要求を出す。
- RFUSEはRFUSEがring channelをmmapし、RFUSE要求を受信する。
- RFUSEはring channelに割当らたworker threadがpending ring bufferのhead pointerをpolling
- RFUSEはpollingのCPU消費を緩和するため一定期間busy waitしたsleep(Hybrid polling)
Write処理(非DirectIO)
アプリからのキャッシュあり(非DirectIO)File Writeop処理はfuse_file_write_iter→fuse_cache_write_iter→fuse_perform_writeから、rfuse_perform_writeに飛ぶ。Writeデータは、fuse_perform_writeで、ユーザ空間アクセス用に一時ページを割当と(rfuse_pages_alloc)、コピー(rfuse_fill_write_pages)する。
flamegraphではどdown_writeのCPU使用率が高いが、これはfuse_cache_write_iter->inode_lock->down_writeである。fioのオプションを変え、ファイルを分散すると以下のようにdown_writeは消える。
ssize_t rfuse_perform_write(struct kiocb *iocb, struct address_space *mapping, struct iov_iter *ii, loff_t pos){
struct inode *inode = mapping->host;
struct fuse_conn *fc = get_fuse_conn(inode);
struct fuse_mount *fm = get_fuse_mount(inode);
struct fuse_inode *fi = get_fuse_inode(inode);
int err = 0;
ssize_t res = 0;
if (inode->i_size < pos + iov_iter_count(ii))
set_bit(FUSE_I_SIZE_UNSTABLE, &fi->state);
do {
ssize_t count;
struct rfuse_io_args ria = {};
struct rfuse_pages *rp = &ria.rp;
struct rfuse_req *r_req;
unsigned int nr_pages = rfuse_wr_pages(pos, iov_iter_count(ii), fc->max_pages);
rp->pages = fuse_pages_alloc(nr_pages, GFP_KERNEL, &rp->descs);★
if (!rp->pages) {
err = -ENOMEM;
break;
}
r_req = rfuse_get_req(fm, false, false);
ria.r_req = r_req;
count = rfuse_fill_write_pages(&ria, mapping, ii, pos, nr_pages);★
if (count <= 0) {
err = count;
} else {
err = rfuse_send_write_pages(&ria, iocb, inode, pos, count);
if (!err) {
struct fuse_write_out *out = (struct fuse_write_out *)&ria.r_req->args;
size_t num_written = out->size;
res += num_written;
pos += num_written;
/* break out of the loop on short write */
if (num_written != count)
err = -EIO;
}
}
rfuse_put_request(r_req);
kfree(rp->pages);
} while (!err && iov_iter_count(ii));
if (res > 0)
fuse_write_update_size(inode, pos);
clear_bit(FUSE_I_SIZE_UNSTABLE, &fi->state);
fuse_invalidate_attr(inode);
return res > 0 ? res : err;
}
Writeデータはrfuse_fill_write_pagesでマップメモリにコピーされる。
static ssize_t rfuse_fill_write_pages(struct rfuse_io_args *ria, struct address_space *mapping,
struct iov_iter *ii, loff_t pos, unsigned int max_pages)
{
struct rfuse_pages *rp = &ria->rp;
struct fuse_conn *fc = get_fuse_conn(mapping->host);
unsigned offset = pos & (PAGE_SIZE - 1);
size_t count = 0;
int err;
ria->r_req->in_pages = true;
rp->descs[0].offset = offset;
do {
size_t tmp;
struct page *page;
pgoff_t index = pos >> PAGE_SHIFT;
size_t bytes = min_t(size_t, PAGE_SIZE - offset,
iov_iter_count(ii));
bytes = min_t(size_t, bytes, fc->max_write - count);
again:
err = -EFAULT;
if (iov_iter_fault_in_readable(ii, bytes))
break;
err = -ENOMEM;
page = grab_cache_page_write_begin(mapping, index, 0);
if (!page)
break;
if (mapping_writably_mapped(mapping))
flush_dcache_page(page);
tmp = copy_page_from_iter_atomic(page, offset, bytes, ii);
flush_dcache_page(page);
if (!tmp) {
unlock_page(page);
put_page(page);
goto again;
}
err = 0;
rp->pages[rp->num_pages] = page;
rp->descs[rp->num_pages].length = tmp;
rp->num_pages++;
count += tmp;
pos += tmp;
offset += tmp;
if (offset == PAGE_SIZE)
offset = 0;
/* If we copied full page, mark it uptodate */
if (tmp == PAGE_SIZE)
SetPageUptodate(page);
if (PageUptodate(page)) {
unlock_page(page);
} else {
ria->write.page_locked = true;
break;
}
if (!fc->big_writes)
break;
} while (iov_iter_count(ii) && count < fc->max_write &&
rp->num_pages < max_pages && offset == 0);
return count > 0 ? count : err;
}
Write処理(DirectIO)
rfuse_direct_ioで、ページ割当(rfuse_io_alloc)し、
The iov_iter interface [LWN.net]
ssize_t rfuse_direct_io(struct fuse_io_priv *io, struct iov_iter *iter,
loff_t *ppos, int flags)
{
int write = flags & FUSE_DIO_WRITE;
int cuse = flags & FUSE_DIO_CUSE;
struct file *file = io->iocb->ki_filp;
struct inode *inode = file->f_mapping->host;
struct fuse_file *ff = file->private_data;
struct fuse_conn *fc = ff->fm->fc;
size_t nmax = write ? fc->max_write : fc->max_read;
loff_t pos = *ppos;
size_t count = iov_iter_count(iter);
pgoff_t idx_from = pos >> PAGE_SHIFT;
pgoff_t idx_to = (pos + count - 1) >> PAGE_SHIFT;
ssize_t res = 0;
int err = 0;
struct rfuse_io_args *ria;
unsigned int max_pages;
max_pages = iov_iter_npages(iter, fc->max_pages);
ria = rfuse_io_alloc(io, max_pages);★rfuse_io_alloc内でライトサイズのページを割り当てる(kzalloc)。
if (!ria)
return -ENOMEM;
ria->io = io;
if (!cuse && rfuse_range_is_writeback(inode, idx_from, idx_to)) {
if (!write)
inode_lock(inode);
rfuse_sync_writes(inode);
if (!write)
inode_unlock(inode);
}
io->should_dirty = !write && iter_is_iovec(iter);
while (count) {
ssize_t nres;
fl_owner_t owner = current->files;
size_t nbytes = min(count, nmax);
err = rfuse_get_user_pages(ria, iter, &nbytes, write,
max_pages);★ユーザ空間のiovページを、rfuse_io_allocで割り当てたページにiov_iter_get_pagesでマップする。
if (err && !nbytes)
break;
if (write) {
nres = rfuse_send_write(ria, pos, nbytes, owner);
} else {
nres = rfuse_send_read(ria, pos, nbytes, owner);
}
if (!io->async || nres < 0) {
rfuse_release_user_pages(&ria->rp, io->should_dirty);
rfuse_io_free(ria);
}
ria = NULL;
if (nres < 0) {
iov_iter_revert(iter, nbytes);
err = nres;
break;
}
WARN_ON(nres > nbytes);
count -= nres;
res += nres;
pos += nres;
if (nres != nbytes) {
iov_iter_revert(iter, nbytes - nres);
break;
}
if (count) {
max_pages = iov_iter_npages(iter, fc->max_pages);
ria = rfuse_io_alloc(io, max_pages);
if (!ria)
break;
}
}
if (ria)
rfuse_io_free(ria);
if (res > 0)
*ppos = pos;
return res > 0 ? res : err;
}
EXPORT_SYMBOL_GPL(rfuse_direct_io);
RFUSEデーモンの処理
本体はfuse_do_work。
static void *fuse_do_work(void *data)
{
struct fuse_worker *w = (struct fuse_worker *) data;
struct fuse_mt *mt = w->mt;
while (!fuse_session_exited(mt->se)) {
int isforget = 0;
int res;
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
res = fuse_session_receive_buf_int(mt->se, &w->fbuf, w->ch);
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
if (res == -EINTR)
continue;
if (res <= 0) {
if (res < 0) {
fuse_session_exit(mt->se);
mt->error = res;
}
break;
}
pthread_mutex_lock(&mt->lock);
if (mt->exit) {
pthread_mutex_unlock(&mt->lock);
return NULL;
}
/*
* This disgusting hack is needed so that zillions of threads
* are not created on a burst of FORGET messages
*/
if (!(w->fbuf.flags & FUSE_BUF_IS_FD)) {
struct fuse_in_header *in = w->fbuf.mem;
if (in->opcode == FUSE_FORGET ||
in->opcode == FUSE_BATCH_FORGET)
isforget = 1;
}
// If it's not forget, this thread should do work
if (!isforget)
mt->numavail--;
// If there are no available workers, it starts a new worker
if (mt->numavail == 0)
fuse_loop_start_thread(mt);
pthread_mutex_unlock(&mt->lock);
fuse_session_process_buf_int(mt->se, &w->fbuf, w->ch);
pthread_mutex_lock(&mt->lock);
if (!isforget)
mt->numavail++; // now this worker is free
if (mt->numavail > mt->max_idle) {
if (mt->exit) {
pthread_mutex_unlock(&mt->lock);
return NULL;
}
list_del_worker(w); // if it exceeds 10 workers, free this worker
mt->numavail--;
mt->numworker--;
pthread_mutex_unlock(&mt->lock);
pthread_detach(w->thread_id);
free(w->fbuf.mem);
fuse_chan_put(w->ch);
free(w);
return NULL;
}
pthread_mutex_unlock(&mt->lock);
}
sem_post(&mt->finish);
return NULL;
}