FUSE (Filesystem in Userspace)
FUSE는 파일시스템(Filesystem) 로직을 커널 밖 사용자 공간(User Space)에서 구현하게 해주는 프레임워크입니다. 이 문서에서는 `/dev/fuse` 요청/응답 프로토콜, fuse.ko와 libfuse의 역할 분리, lookup/read/write/flush 연산 경로, page cache와 writeback 캐시(Cache) 정책, 권한 위임과 uid/gid 매핑(Mapping), virtiofs 기반 가상화(Virtualization) 시나리오, 컨텍스트 스위치 오버헤드(Overhead)와 병목(Bottleneck) 최적화 기법을 상세히 다룹니다.
핵심 요약
- FUSE (Filesystem in Userspace) — 커널 모듈 없이 유저스페이스 프로그램으로 파일시스템을 구현할 수 있는 프레임워크입니다.
- /dev/fuse 디바이스 — 커널 FUSE 모듈과 유저스페이스 데몬(Daemon) 사이의 통신 채널 역할을 합니다.
- 커널-유저스페이스 경계 — VFS 요청이 커널에서 유저스페이스로 전달되고 응답이 돌아오는 왕복 경로를 이해해야 합니다.
- fuse_conn / fuse_req — 커널 측에서 FUSE 연결 상태와 개별 요청을 관리하는 핵심 구조체입니다.
- libfuse API — 유저스페이스에서 FUSE 파일시스템을 구현할 때 사용하는 라이브러리로, high-level/low-level API를 제공합니다.
- FUSE 데몬 생명주기 — 마운트(Mount) → 요청 처리 루프 → 언마운트(Unmount) 과정에서 데몬의 상태 전이를 파악합니다.
- Passthrough 모드 — 데이터를 유저스페이스 경유 없이 커널에서 직접 처리하여 성능 오버헤드를 제거하는 최적화 기능입니다.
- virtiofs 통합 — 가상 머신(VM) 환경에서 호스트 파일시스템을 게스트에 효율적으로 공유하는 FUSE 기반 기술입니다.
단계별 이해
- FUSE 3계층 구조 파악 — 커널 모듈, /dev/fuse, 유저스페이스 데몬의 역할을 구분합니다.
VFS → FUSE 커널 모듈 → /dev/fuse → 유저스페이스 데몬 경로에서 각 계층의 책임 범위를 이해합니다.
- 요청-응답 프로토콜 이해 — 커널과 유저스페이스 사이의 메시지 교환 과정을 추적합니다.
fuse_req가 큐에 들어가고, 데몬이read()로 가져가 처리한 뒤write()로 응답하는 흐름을 따라갑니다. - libfuse로 간단한 FS 구현 — high-level API를 사용하여 최소한의 FUSE 파일시스템을 직접 작성해봅니다.
getattr,readdir,open,read콜백만 구현하면 기본적인 읽기 전용 파일시스템을 만들 수 있습니다. - 성능 특성 분석 — 유저스페이스 경유로 인한 오버헤드와 최적화 방안을 비교합니다.
컨텍스트 스위치(Context Switch) 비용, splice/passthrough 모드, writeback 캐시 등 성능 개선 기법을 검토합니다.
- 실제 활용 사례 탐구 — SSHFS, GlusterFS, virtiofs 등 실전 FUSE 파일시스템의 구조를 살펴봅니다.
각 사례에서 FUSE의 장점(개발 용이성, 안정성)과 한계(성능 오버헤드)가 어떻게 균형을 이루는지 확인합니다.
FUSE 개요
FUSE(Filesystem in Userspace)는 커널 모듈(Kernel Module)을 직접 작성하지 않고도 유저스페이스 프로그램으로 파일시스템을 구현할 수 있게 해주는 프레임워크입니다. 2005년 Linux 2.6.14에서 mainline에 통합되었으며, 현재까지 수백 개의 FUSE 기반 파일시스템이 활발히 사용되고 있습니다.
유저스페이스 파일시스템이 필요한 이유
- 개발 용이성 — 커널 프로그래밍 없이 C, Python, Go, Rust 등 일반 언어로 파일시스템 구현 가능
- 안정성 — 유저스페이스 프로세스(Process) 크래시가 커널 패닉(Kernel Panic)을 유발하지 않음
- 빠른 프로토타이핑 — 커널 재컴파일 없이 파일시스템 로직을 즉시 수정/테스트
- 라이브러리 활용 — OpenSSL, libcurl, gRPC 등 유저스페이스 라이브러리를 자유롭게 사용
- non-root 마운트(Mount) — 일반 사용자도 fusermount를 통해 파일시스템 마운트 가능
FUSE 3계층 구조
FUSE는 세 가지 핵심 컴포넌트로 구성됩니다:
| 계층 | 컴포넌트 | 역할 |
|---|---|---|
| 커널 | fuse.ko 모듈 | VFS 요청을 /dev/fuse 디바이스를 통해 유저스페이스로 전달 |
| 라이브러리 | libfuse (libfuse3) | /dev/fuse와의 통신을 추상화, high-level/low-level API 제공 |
| 유저스페이스 | FUSE 데몬 | 실제 파일시스템 로직 구현 (sshfs, ntfs-3g 등) |
FUSE 아키텍처
요청/응답 흐름 상세
- 애플리케이션이
open(),read()등 시스템 콜(System Call) 호출 - VFS가 FUSE 파일시스템의
file_operations/inode_operations을 통해 FUSE 커널 모듈로 디스패치(Dispatch) - FUSE 커널 모듈이
fuse_req구조체(Struct)를 생성하여fuse_iqueue에 큐잉 - 유저 데몬이
/dev/fuse에서read()로 요청을 가져옴 - 유저 데몬이 요청을 처리 (파일 읽기, 네트워크 I/O 등)
- 유저 데몬이
/dev/fuse에write()로 응답을 기록 - FUSE 커널 모듈이 대기 중인 프로세스를 깨우고 결과 반환
FUSE 커널 모듈 내부
/dev/fuse 디바이스
/dev/fuse는 캐릭터 디바이스(major 10, minor 229)로, FUSE 커널 모듈과 유저스페이스 데몬 간의 통신 채널입니다. 데몬은 이 디바이스를 열어 read()로 요청을 수신하고 write()로 응답을 전송합니다.
/* fs/fuse/dev.c — /dev/fuse file operations */
const struct file_operations fuse_dev_operations = {
.owner = THIS_MODULE,
.open = fuse_dev_open,
.read = fuse_dev_read, /* 유저 데몬이 요청을 읽음 */
.write = fuse_dev_write, /* 유저 데몬이 응답을 기록 */
.splice_read = fuse_dev_splice_read,
.splice_write = fuse_dev_splice_write,
.poll = fuse_dev_poll,
.release = fuse_dev_release,
.fasync = fuse_dev_fasync,
};
fuse_conn (연결 관리)
struct fuse_conn은 하나의 FUSE 마운트와 연결된 유저 데몬 사이의 모든 상태를 관리하는 핵심 구조체입니다.
/* include/linux/fuse.h — 주요 필드 발췌 */
struct fuse_conn {
unsigned max_read; /* 최대 read 크기 */
unsigned max_write; /* 최대 write 크기 */
unsigned max_pages; /* 단일 요청 최대 페이지 수 */
unsigned max_background; /* 최대 배경 요청 수 */
unsigned congestion_threshold; /* 혼잡 시작 임계치 */
unsigned num_background; /* 현재 배경 요청 수 */
struct fuse_iqueue iq; /* 입력 큐 (pending 요청) */
spinlock_t lock; /* 연결 상태 보호 */
unsigned minor; /* FUSE 프로토콜 마이너 버전 */
unsigned conn_init:1; /* FUSE_INIT 완료 여부 */
unsigned writeback_cache:1; /* writeback 캐시 활성화 */
unsigned no_open:1; /* OPEN 요청 생략 가능 */
unsigned parallel_dirops:1; /* 병렬 디렉토리 연산 */
};
fuse_iqueue / fuse_pqueue (요청/응답 큐)
FUSE는 두 가지 큐를 사용하여 요청을 관리합니다:
- fuse_iqueue (입력 큐) — 커널에서 유저스페이스로 전달 대기 중인 요청. 데몬의
read()호출 시 이 큐에서 요청을 꺼냄 - fuse_pqueue (처리 큐) — 유저스페이스에서 처리 중인 요청. 데몬의
write()응답과 매칭
요청 라이프사이클
/* struct fuse_req 주요 필드 */
struct fuse_req {
struct list_head list; /* 큐 연결 */
u64 unique; /* 고유 요청 ID */
struct fuse_in_header in; /* 요청 헤더 */
struct fuse_out_header out; /* 응답 헤더 */
wait_queue_head_t waitq; /* 완료 대기 큐 */
struct fuse_args *args; /* 요청/응답 인자 */
unsigned isreply:1; /* 응답 필요 여부 */
unsigned force:1; /* 연결 중단 시에도 전송 */
unsigned background:1; /* 배경 요청 여부 */
};
/*
* 요청 라이프사이클:
* 1. fuse_simple_request() / fuse_simple_background()로 생성
* 2. fuse_iqueue의 pending 리스트에 추가
* 3. 데몬이 /dev/fuse read()로 요청 수신 → fuse_pqueue로 이동
* 4. 데몬이 /dev/fuse write()로 응답 → fuse_req 완료
* 5. 대기 중인 커널 스레드 깨움 → 결과 반환
*/
FUSE 프로토콜
fuse_in_header / fuse_out_header
모든 FUSE 메시지는 헤더로 시작합니다:
/* include/uapi/linux/fuse.h */
struct fuse_in_header {
uint32_t len; /* 메시지 전체 길이 (헤더 포함) */
uint32_t opcode; /* 작업 코드 (FUSE_LOOKUP, FUSE_READ 등) */
uint64_t unique; /* 고유 요청 ID (응답 매칭) */
uint64_t nodeid; /* 대상 inode 번호 */
uint32_t uid; /* 요청자 UID */
uint32_t gid; /* 요청자 GID */
uint32_t pid; /* 요청자 PID */
uint32_t padding;
};
struct fuse_out_header {
uint32_t len; /* 응답 전체 길이 */
int32_t error; /* 에러 코드 (0 = 성공, 음수 = errno) */
uint64_t unique; /* 요청의 unique와 매칭 */
};
주요 opcode 테이블
| Opcode | 값 | 설명 |
|---|---|---|
FUSE_LOOKUP | 1 | 경로명으로 inode 탐색 |
FUSE_FORGET | 2 | inode 참조 카운트(Reference Count) 감소 (응답 없음) |
FUSE_GETATTR | 3 | 파일 속성 조회 (stat) |
FUSE_SETATTR | 4 | 파일 속성 변경 (chmod, chown, truncate) |
FUSE_OPEN | 14 | 파일 열기 |
FUSE_READ | 15 | 파일 읽기 |
FUSE_WRITE | 16 | 파일 쓰기 |
FUSE_RELEASE | 18 | 파일 닫기 |
FUSE_OPENDIR | 27 | 디렉토리 열기 |
FUSE_READDIR | 28 | 디렉토리 목록 읽기 |
FUSE_RELEASEDIR | 29 | 디렉토리 닫기 |
FUSE_MKDIR | 9 | 디렉토리 생성 |
FUSE_UNLINK | 10 | 파일 삭제 |
FUSE_RMDIR | 11 | 디렉토리 삭제 |
FUSE_RENAME | 12 | 파일/디렉토리 이름 변경 |
FUSE_LINK | 13 | 하드 링크 생성 |
FUSE_SYMLINK | 6 | 심볼릭 링크 생성 |
FUSE_CREATE | 35 | 파일 생성 + 열기 (atomic) |
FUSE_STATFS | 17 | 파일시스템 통계 |
FUSE_INIT | 26 | 연결 초기화 (핸드셰이크) |
FUSE_DESTROY | 38 | 연결 종료 |
FUSE_NOTIFY_REPLY | 41 | 비동기 알림 응답 |
FUSE_READDIRPLUS | 44 | readdir + lookup 결합 (성능 최적화) |
FUSE_INIT 핸드셰이크
마운트 시 커널과 유저 데몬 사이에 FUSE_INIT 메시지를 교환하여 프로토콜 버전과 기능 플래그를 협상합니다:
/* FUSE_INIT 요청/응답 */
struct fuse_init_in {
uint32_t major; /* 커널이 지원하는 메이저 버전 (7) */
uint32_t minor; /* 커널이 지원하는 마이너 버전 */
uint32_t max_readahead;
uint32_t flags; /* 커널 지원 기능 플래그 */
uint32_t flags2; /* 확장 플래그 (7.36+) */
};
struct fuse_init_out {
uint32_t major; /* 데몬이 선택한 메이저 버전 */
uint32_t minor; /* 데몬이 선택한 마이너 버전 */
uint32_t max_readahead;
uint32_t flags; /* 데몬이 활성화할 기능 */
uint32_t max_background;
uint32_t congestion_threshold;
uint32_t max_write;
uint32_t max_pages; /* 7.28+ */
};
/* 주요 기능 플래그 */
#define FUSE_WRITEBACK_CACHE (1 << 16) /* writeback 캐시 */
#define FUSE_PARALLEL_DIROPS (1 << 18) /* 병렬 디렉토리 연산 */
#define FUSE_PASSTHROUGH (1 << 26) /* passthrough I/O (6.9+) */
#define FUSE_DO_READDIRPLUS (1 << 13) /* READDIRPLUS 지원 */
#define FUSE_SPLICE_READ (1 << 7) /* splice로 읽기 */
#define FUSE_SPLICE_WRITE (1 << 8) /* splice로 쓰기 */
FUSE_NOTIFY: 커널 → 유저 비동기 알림
커널은 유저 데몬에 비동기 알림을 보낼 수 있습니다. 이는 캐시 무효화(Invalidation) 등에 사용됩니다:
FUSE_NOTIFY_INVAL_INODE— inode 캐시 무효화FUSE_NOTIFY_INVAL_ENTRY— dentry 캐시 무효화FUSE_NOTIFY_STORE— 커널 캐시에 데이터 저장FUSE_NOTIFY_RETRIEVE— 커널 캐시에서 데이터 조회FUSE_NOTIFY_DELETE— 디렉토리 엔트리 삭제 알림
libfuse API
High-level API (경로 기반)
High-level API는 파일 경로를 기반으로 동작하며, 대부분의 FUSE 파일시스템이 이 API를 사용합니다:
/* fuse_operations: high-level API 콜백 테이블 */
struct fuse_operations {
int (*getattr)(const char *path, struct stat *st,
struct fuse_file_info *fi);
int (*readdir)(const char *path, void *buf,
fuse_fill_dir_t filler, off_t offset,
struct fuse_file_info *fi,
enum fuse_readdir_flags flags);
int (*open)(const char *path, struct fuse_file_info *fi);
int (*read)(const char *path, char *buf, size_t size,
off_t offset, struct fuse_file_info *fi);
int (*write)(const char *path, const char *buf,
size_t size, off_t offset,
struct fuse_file_info *fi);
int (*mkdir)(const char *path, mode_t mode);
int (*unlink)(const char *path);
int (*rmdir)(const char *path);
int (*rename)(const char *from, const char *to,
unsigned int flags);
int (*truncate)(const char *path, off_t size,
struct fuse_file_info *fi);
int (*create)(const char *path, mode_t mode,
struct fuse_file_info *fi);
int (*release)(const char *path, struct fuse_file_info *fi);
int (*fsync)(const char *path, int isdatasync,
struct fuse_file_info *fi);
int (*statfs)(const char *path, struct statvfs *st);
void *(*init)(struct fuse_conn_info *conn,
struct fuse_config *cfg);
void (*destroy)(void *private_data);
/* ... */
};
Low-level API (inode 기반)
Low-level API는 inode 번호(nodeid)를 직접 다루며, 더 세밀한 제어가 가능합니다. 고성능 FUSE 파일시스템에서 사용합니다:
/* fuse_lowlevel_ops: low-level API 콜백 */
struct fuse_lowlevel_ops {
void (*lookup)(fuse_req_t req, fuse_ino_t parent,
const char *name);
void (*getattr)(fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi);
void (*open)(fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi);
void (*read)(fuse_req_t req, fuse_ino_t ino,
size_t size, off_t off,
struct fuse_file_info *fi);
void (*write)(fuse_req_t req, fuse_ino_t ino,
const char *buf, size_t size,
off_t off, struct fuse_file_info *fi);
void (*forget)(fuse_req_t req, fuse_ino_t ino,
uint64_t nlookup);
/* ... */
};
/* Low-level API에서 응답은 명시적으로 전송해야 함 */
fuse_reply_entry(req, &entry_param); /* LOOKUP 응답 */
fuse_reply_buf(req, buf, size); /* READ 응답 */
fuse_reply_err(req, errno); /* 에러 응답 */
fuse_session과 이벤트 루프(Event Loop)
/* 싱글스레드 이벤트 루프 */
struct fuse_session *se = fuse_session_new(&args, &ops,
sizeof(ops), userdata);
fuse_session_mount(se, mountpoint);
fuse_session_loop(se); /* 싱글스레드 */
/* 멀티스레드 이벤트 루프 */
struct fuse_loop_config config = {
.clone_fd = 1, /* /dev/fuse fd를 복제하여 병렬 처리 */
.max_idle_threads = 10,
};
fuse_session_loop_mt(se, &config); /* 멀티스레드 */
FUSE 파일시스템 구현 예제
최소한의 FUSE 파일시스템 예제입니다. 읽기 전용(Read-Only)으로 /hello 파일 하나를 제공합니다:
#define FUSE_USE_VERSION 31
#include <fuse3/fuse.h>
#include <string.h>
#include <errno.h>
static const char *hello_str = "Hello, FUSE!\\n";
static const char *hello_path = "/hello";
static int hello_getattr(const char *path, struct stat *st,
struct fuse_file_info *fi)
{
(void) fi;
memset(st, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {
st->st_mode = S_IFDIR | 0755;
st->st_nlink = 2;
} else if (strcmp(path, hello_path) == 0) {
st->st_mode = S_IFREG | 0444;
st->st_nlink = 1;
st->st_size = strlen(hello_str);
} else {
return -ENOENT;
}
return 0;
}
static int hello_readdir(const char *path, void *buf,
fuse_fill_dir_t filler, off_t offset,
struct fuse_file_info *fi,
enum fuse_readdir_flags flags)
{
(void) offset; (void) fi; (void) flags;
if (strcmp(path, "/") != 0)
return -ENOENT;
filler(buf, ".", NULL, 0, 0);
filler(buf, "..", NULL, 0, 0);
filler(buf, "hello", NULL, 0, 0);
return 0;
}
static int hello_open(const char *path,
struct fuse_file_info *fi)
{
if (strcmp(path, hello_path) != 0)
return -ENOENT;
if ((fi->flags & O_ACCMODE) != O_RDONLY)
return -EACCES;
return 0;
}
static int hello_read(const char *path, char *buf,
size_t size, off_t offset,
struct fuse_file_info *fi)
{
size_t len;
(void) fi;
if (strcmp(path, hello_path) != 0)
return -ENOENT;
len = strlen(hello_str);
if ((size_t)offset < len) {
if (offset + size > len)
size = len - offset;
memcpy(buf, hello_str + offset, size);
} else {
size = 0;
}
return size;
}
static const struct fuse_operations hello_oper = {
.getattr = hello_getattr,
.readdir = hello_readdir,
.open = hello_open,
.read = hello_read,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &hello_oper, NULL);
}
빌드 및 실행
# 빌드 (libfuse3 필요)
gcc -Wall hello_fuse.c `pkg-config --cflags --libs fuse3` -o hello_fuse
# 마운트 포인트 생성 및 실행
mkdir -p /tmp/fuse_mnt
./hello_fuse /tmp/fuse_mnt
# 테스트
ls /tmp/fuse_mnt/ # hello
cat /tmp/fuse_mnt/hello # Hello, FUSE!
# 언마운트
fusermount3 -u /tmp/fuse_mnt
마운트 메커니즘
fusermount3 / mount.fuse3
fusermount3는 non-root 사용자가 FUSE 파일시스템을 마운트/언마운트할 수 있게 해주는 setuid 헬퍼 프로그램입니다. 내부적으로 /dev/fuse를 열고, mount(2) 시스템 콜을 호출한 뒤, fd를 FUSE 데몬에 전달합니다.
/etc/fuse.conf
# /etc/fuse.conf — FUSE 전역 설정
user_allow_other # allow_other 마운트 옵션 허용 (다른 사용자 접근)
mount_max = 1000 # 사용자당 최대 마운트 수
마운트 옵션 테이블
| 옵션 | 설명 | 기본값 |
|---|---|---|
allow_other | 마운트한 사용자 외 다른 사용자도 접근 허용 | 비활성 |
allow_root | root만 추가 접근 허용 | 비활성 |
default_permissions | 커널이 권한 검사 수행 (데몬에 위임하지 않음) | 비활성 |
max_read=N | 단일 read 요청 최대 크기 (바이트) | 131072 |
max_write=N | 단일 write 요청 최대 크기 (바이트) | 131072 |
max_background=N | 최대 동시 배경 요청 수 | 12 |
congestion_threshold=N | BDI 혼잡 시작 임계치 | 9 |
nonempty | 비어있지 않은 디렉토리에도 마운트 허용 | 비활성 |
blkdev | 블록 디바이스 위에 마운트 | 비활성 |
subtype=TYPE | /proc/mounts에 표시할 서브타입 (예: fuse.sshfs) | 없음 |
/proc/filesystems 확인
# FUSE 커널 모듈 로드 확인
grep fuse /proc/filesystems
# nodev fuse
# nodev fuseblk
# 현재 FUSE 마운트 확인
mount -t fuse,fuse.sshfs,fuseblk
# 또는
grep fuse /proc/mounts
성능 특성과 최적화
유저-커널 컨텍스트 스위칭(Context Switching) 오버헤드
FUSE의 근본적인 성능 제약은 모든 파일시스템 요청이 커널 ↔ 유저스페이스 간 컨텍스트 스위칭을 수반한다는 점입니다. 단일 read() 호출이 최소 4번의 컨텍스트 스위칭을 발생시킵니다:
- 애플리케이션 → 커널 (syscall)
- 커널 → FUSE 데몬 (/dev/fuse read)
- FUSE 데몬 → 커널 (/dev/fuse write)
- 커널 → 애플리케이션 (syscall return)
FUSE writeback cache (kernel 3.15+)
writeback cache를 활성화하면 커널이 쓰기 데이터를 페이지 캐시(Page Cache)에 버퍼(Buffer)링하여 배치 처리합니다. FUSE_INIT 시 FUSE_WRITEBACK_CACHE 플래그로 활성화합니다.
writeback cache는 성능을 크게 향상시키지만, 크래시 시 데이터 일관성 보장이 약해집니다. 데몬이 정상 종료하지 않으면 페이지 캐시의 더티 데이터가 유실될 수 있습니다.
splice/zero-copy 전송
splice를 사용하면 유저스페이스 버퍼 복사 없이 커널 파이프를 통해 데이터를 전달할 수 있습니다. FUSE_SPLICE_READ/FUSE_SPLICE_WRITE 플래그로 활성화합니다.
max_background / congestion_threshold 튜닝
# 높은 병렬성이 필요한 워크로드에서 배경 요청 수 증가
./my_fuse_fs -o max_background=64,congestion_threshold=48 /mnt/fuse
# sysfs를 통한 런타임 확인
cat /sys/fs/fuse/connections/<N>/max_background
cat /sys/fs/fuse/connections/<N>/congestion_threshold
FUSE passthrough (kernel 6.9+)
FUSE passthrough는 데이터 I/O를 유저스페이스 데몬을 거치지 않고 커널에서 직접 백엔드 파일에 전달하는 기능입니다. 메타데이터 연산만 데몬이 처리하고, 실제 데이터 읽기/쓰기는 커널이 직접 수행하여 네이티브 파일시스템에 근접한 성능을 달성합니다.
/* passthrough 설정 (FUSE daemon 측) */
/* FUSE_INIT에서 FUSE_PASSTHROUGH 플래그 활성화 후 */
/* OPEN 응답 시 backing file의 fd를 전달 */
struct fuse_file_info fi;
fi.passthrough_fh = backing_fd; /* 백엔드 파일 디스크립터 */
fi.direct_io = 0;
/* 이후 READ/WRITE는 커널이 backing_fd를 통해 직접 처리 */
벤치마크 비교
| 항목 | native ext4 | FUSE (기본) | FUSE (writeback) | FUSE (passthrough) | virtiofs (DAX) |
|---|---|---|---|---|---|
| 순차 읽기 (MB/s) | ~3,500 | ~1,200 | ~1,500 | ~3,200 | ~3,000 |
| 순차 쓰기 (MB/s) | ~2,000 | ~600 | ~1,400 | ~1,800 | ~1,700 |
| 랜덤 4K IOPS (읽기) | ~350K | ~30K | ~50K | ~300K | ~250K |
| 메타데이터 ops/s | ~200K | ~15K | ~15K | ~15K | ~80K |
위 수치는 NVMe SSD 기준 대략적인 참고값입니다. 실제 성능은 FUSE 데몬 구현, 워크로드 패턴, 시스템 구성에 따라 크게 달라집니다.
virtiofs (가상화 환경 FUSE)
아키텍처
virtiofs는 FUSE 프로토콜을 virtio 전송 계층 위에서 사용하여, 호스트와 게스트 VM 간에 고성능 파일시스템 공유를 제공합니다. 호스트 측에서 virtiofsd 데몬이 실행되고, 게스트에서 virtio-fs 커널 드라이버가 FUSE 요청을 virtio 큐를 통해 전달합니다.
DAX (Direct Access) 모드
DAX 모드에서는 호스트의 페이지 캐시를 게스트 VM에 직접 매핑하여 데이터 복사를 제거합니다. 메모리 매핑된 파일 접근이 네이티브에 가까운 성능을 달성합니다.
QEMU/libvirt 설정 예제
# 1. virtiofsd 데몬 실행 (Rust 구현, 권장)
/usr/libexec/virtiofsd \
--socket-path=/tmp/virtiofs.sock \
--shared-dir=/path/to/shared \
--cache=always
# 2. QEMU에서 virtiofs 디바이스 추가
qemu-system-x86_64 \
-chardev socket,id=char0,path=/tmp/virtiofs.sock \
-device vhost-user-fs-pci,chardev=char0,tag=myfs \
-object memory-backend-memfd,id=mem,size=4G,share=on \
-numa node,memdev=mem \
...
# 3. 게스트 내부에서 마운트
mount -t virtiofs myfs /mnt/shared
컨테이너(Container) 환경 활용
Kata Containers는 virtiofs를 사용하여 컨테이너 이미지와 볼륨을 경량 VM 내부에 공유합니다. 이를 통해 컨테이너 수준의 편의성과 VM 수준의 격리(Isolation)를 동시에 달성합니다.
보안 모델
non-root 마운트와 보안 고려사항
FUSE는 일반 사용자의 파일시스템 마운트 시 가장 널리 사용되는 커널 메커니즘 중 하나입니다. 이로 인해 다음과 같은 보안 고려사항이 존재합니다:
- 기본 접근 제한 — FUSE 마운트는 기본적으로 마운트한 사용자만 접근 가능 (다른 사용자/root도 접근 불가)
- 데몬 신뢰성 — FUSE 데몬이 악의적인 응답을 반환할 수 있음 (예: getattr에서 거짓 권한 반환)
- DoS 가능성 — 데몬이 응답하지 않으면 해당 마운트에 접근하는 프로세스가 영구 대기(hang)
default_permissions vs 커스텀 접근 제어(Access Control)
| 모드 | 권한 검사 위치 | 특징 |
|---|---|---|
default_permissions |
커널 (VFS) | 표준 Unix 권한 모델 (uid/gid/mode). 안전하고 예측 가능 |
| 커스텀 (기본) | FUSE 데몬 | 데몬이 자체 ACL/인증 로직 구현 가능. 유연하지만 데몬 버그 시 보안 취약 |
allow_other / allow_root
allow_other— 모든 사용자가 마운트 포인트에 접근 가능./etc/fuse.conf에user_allow_other가 설정되어야 사용 가능allow_root— root만 추가 접근 허용.allow_other와 동시 사용 불가
namespace 격리 (user namespace + FUSE)
Linux 4.18+에서는 user namespace 내에서 FUSE 마운트가 가능합니다. 이를 통해 비특권 컨테이너에서도 FUSE 파일시스템을 사용할 수 있습니다:
# unprivileged user namespace에서 FUSE 마운트
unshare --user --mount --map-root-user -- bash -c \
"./my_fuse_fs /mnt/fuse"
신뢰할 수 없는 FUSE 데몬 방어
FUSE 데몬 보안 주의사항:
- 데몬이
FUSE_LOOKUP에 대해 악의적 inode 정보를 반환할 수 있음 →default_permissions사용 권장 - 데몬이 응답을 지연하여 프로세스 행(hang) 유발 가능 →
SIGKILL로 데몬 강제 종료 후fusermount -u - 심볼릭 링크를 통한 경로 탈출 가능 → 데몬이 마운트 포인트 외부를 가리키는 symlink를 반환할 수 있음
ptrace기반 공격 — FUSE 마운트에 접근하는 특권 프로세스를 악용할 수 있음
실제 FUSE 파일시스템
| 프로젝트 | 용도 | 특징 | API |
|---|---|---|---|
| sshfs | SSH를 통한 원격 파일시스템 | SFTP 프로토콜 기반, 간편한 원격 마운트 | libfuse3 |
| ntfs-3g | NTFS 읽기/쓰기 | Windows NTFS 파티션 완전 지원 | libfuse (low-level) |
| s3fs-fuse | Amazon S3 버킷 마운트 | S3 API를 파일시스템으로 매핑 | libfuse3 |
| rclone mount | 40+ 클라우드 스토리지 | Google Drive, Dropbox, S3, Azure 등 통합 | Go FUSE (bazil/cgofuse) |
| GlusterFS | 분산 파일시스템 | FUSE 클라이언트 + 네이티브 프로토콜 | libfuse |
| CephFS (FUSE) | 분산 파일시스템 | 커널 클라이언트 대안, 더 빠른 버그픽스 배포 | libfuse |
| gocryptfs | 암호화(Encryption) 파일시스템 | 파일 단위 AES-256-GCM 암호화 | Go FUSE |
| mergerfs | 디스크 풀링 | 여러 디스크를 하나의 마운트로 통합 | libfuse3 |
| SSHFS (Rust) | SSH 원격 마운트 | Rust fuse3 라이브러리 기반 재구현 | fuse3 (Rust) |
FUSE 관점의 Ceph 연계
Ceph는 여러 접근 경로를 제공하며, 이 중 FUSE와 직접 연결되는 경로는 ceph-fuse입니다. 반면 Ceph 커널 클라이언트(fs/ceph/)와 RBD(drivers/block/rbd.c)는 FUSE를 거치지 않는 별도 경로입니다. 따라서 본 문서에서는 FUSE와의 접점만 요약합니다.
| 경로 | 전송 경로 | FUSE 사용 여부 | 특징 |
|---|---|---|---|
| ceph-fuse | VFS → FUSE → 유저 데몬 → Ceph | 사용 | 배포/디버깅(Debugging)이 유연, 커널 업데이트 의존도 낮음 |
| CephFS 커널 클라이언트 | VFS → fs/ceph → Ceph | 미사용 | 커널 경로 직결, 컨텍스트 스위칭 오버헤드 감소 |
| RBD | 블록 계층 → rbd 모듈 → Ceph | 미사용 | 파일시스템이 아닌 블록 디바이스 경로 |
문맥 구분: FUSE 성능/디버깅 이슈를 분석할 때는 ceph-fuse 경로만 같은 범주로 비교해야 합니다. 커널 클라이언트나 RBD 수치는 FUSE 오버헤드 분석에 직접 대입하면 왜곡됩니다.
ceph-fuse 운영 체크포인트(Checkpoint)
- 지연 급증 —
/sys/fs/fuse/connections/*/waiting으로 대기 요청 누적 여부 확인 - 백그라운드 혼잡 —
max_background,congestion_threshold조정 - 데몬 병목 —
strace -f -e read,write -p <PID>로/dev/fuse왕복 지연 확인
Ceph 자체 아키텍처(MON/MDS/OSD, CRUSH, RBD, libceph)는 FUSE 주제와 분리해 다루는 것이 문맥적으로 정확합니다.
디버깅과 트러블슈팅
기본 디버깅 옵션
# -d: 디버그 모드 (모든 FUSE 메시지를 stderr에 출력, 포그라운드 실행 포함)
./my_fuse_fs -d /mnt/fuse
# -f: 포그라운드 실행 (데몬화하지 않음)
./my_fuse_fs -f /mnt/fuse
# -s: 싱글스레드 모드 (디버깅 시 유용)
./my_fuse_fs -f -s /mnt/fuse
강제 언마운트
# 정상 언마운트
fusermount3 -u /mnt/fuse
# 데몬이 응답하지 않을 때 강제 언마운트
fusermount3 -uz /mnt/fuse # lazy unmount (-z)
# root 권한으로 강제 언마운트
umount -l /mnt/fuse # lazy unmount
umount -f /mnt/fuse # force unmount
strace로 /dev/fuse 트래픽 추적
# FUSE 데몬의 /dev/fuse 통신 추적
strace -f -e read,write -p <FUSE_DAEMON_PID>
# 특정 파일 디스크립터만 추적 (fd 번호 확인 후)
ls -la /proc/<PID>/fd/ | grep /dev/fuse
strace -f -e trace=read,write -e read=<fd> -e write=<fd> -p <PID>
/sys/fs/fuse/connections/ 디버그 인터페이스
# FUSE 연결 목록 확인
ls /sys/fs/fuse/connections/
# 42/ 43/ ...
# 연결 상태 확인
cat /sys/fs/fuse/connections/42/waiting # 대기 중인 요청 수
cat /sys/fs/fuse/connections/42/max_background
cat /sys/fs/fuse/connections/42/congestion_threshold
# 연결 강제 중단 (데몬 hang 시)
echo 1 > /sys/fs/fuse/connections/42/abort
/sys/fs/fuse/connections/<N>/abort에 쓰기를 하면 해당 FUSE 연결의 모든 대기 중인 요청이 즉시 에러로 완료됩니다. 데몬이 행(hang)되어 마운트에 접근하는 프로세스가 블록될 때 유용합니다.
/dev/fuse 프로토콜
/dev/fuse는 커널과 유저스페이스 FUSE 데몬 사이의 메시지 전달 채널입니다. 이 캐릭터 디바이스를 통해 주고받는 메시지의 형식과 시맨틱을 깊이 이해해야 고성능 FUSE 파일시스템을 구현할 수 있습니다.
메시지 형식 상세
모든 FUSE 메시지는 고정 크기 헤더 + 가변 크기 페이로드(Payload)로 구성됩니다. 요청(커널→데몬)은 fuse_in_header로 시작하고, 응답(데몬→커널)은 fuse_out_header로 시작합니다. unique 필드가 요청-응답 매칭의 핵심입니다.
opcode별 요청/응답 페이로드
| Opcode | 요청 구조체 | 응답 구조체 | 비고 |
|---|---|---|---|
FUSE_LOOKUP | 이름 문자열 (null-terminated) | fuse_entry_out | 응답에 attr timeout 포함 |
FUSE_FORGET | fuse_forget_in | 없음 (no reply) | nlookup 감소 |
FUSE_GETATTR | fuse_getattr_in | fuse_attr_out | attr_valid 시간 포함 |
FUSE_OPEN | fuse_open_in | fuse_open_out | fh (파일 핸들) 반환 |
FUSE_READ | fuse_read_in | raw 데이터 바이트 | size만큼 데이터 반환 |
FUSE_WRITE | fuse_write_in + 데이터 | fuse_write_out | 실제 기록 바이트 수 |
FUSE_CREATE | fuse_create_in + 이름 | fuse_entry_out + fuse_open_out | lookup+open 원자적(Atomic) |
FUSE_SETATTR | fuse_setattr_in | fuse_attr_out | valid 비트마스크로 변경 필드 지정 |
FUSE_READDIR | fuse_read_in | fuse_dirent 배열 | 각 엔트리 8바이트 정렬 |
FUSE_READDIRPLUS | fuse_read_in | fuse_direntplus 배열 | readdir + 각 엔트리 attr 포함 |
FUSE_INIT | fuse_init_in | fuse_init_out | 프로토콜 버전/기능 협상 |
FUSE_BATCH_FORGET | fuse_batch_forget_in | 없음 | 여러 inode 일괄 forget |
다중 리더와 clone_fd
기본적으로 FUSE 데몬은 단일 /dev/fuse fd에서 read()를 호출합니다. 멀티스레드 처리 시 여러 스레드(Thread)가 동일 fd에서 경쟁적으로 읽기를 수행합니다. Linux 4.2+에서 도입된 clone_fd 옵션은 각 스레드에 독립적인 fd를 제공하여 락 경합(Contention)을 줄입니다.
/* clone_fd를 사용한 멀티스레드 FUSE */
struct fuse_loop_config config = {
.clone_fd = 1, /* 각 스레드에 별도 fd 할당 */
.max_idle_threads = 10, /* 유휴 스레드 풀 크기 */
};
fuse_session_loop_mt(se, &config);
/* 커널 측: FUSE_DEV_IOC_CLONE ioctl로 fd 복제 */
int cloned_fd = open("/dev/fuse", O_RDWR);
ioctl(cloned_fd, FUSE_DEV_IOC_CLONE, &original_fd);
FUSE_INTERRUPT: 요청 취소 메커니즘
애플리케이션이 시그널(Signal)로 인해 시스템 콜을 중단하면, 커널은 FUSE_INTERRUPT 메시지를 데몬에 보냅니다. 데몬은 해당 요청을 취소하고 -EINTR로 응답할 수 있습니다.
/* FUSE_INTERRUPT 요청 구조 */
struct fuse_interrupt_in {
uint64_t unique; /* 취소할 요청의 unique ID */
};
/* 데몬 측 처리 패턴 */
void handle_interrupt(fuse_req_t req, void *data)
{
struct pending_op *op = data;
pthread_mutex_lock(&op->lock);
if (!op->completed) {
op->cancelled = 1;
pthread_cond_signal(&op->cond);
}
pthread_mutex_unlock(&op->lock);
}
/* 인터럽트 핸들러 등록 */
fuse_req_interrupt_func(req, handle_interrupt, op);
splice 기반 Zero-Copy 전송
일반적인 FUSE I/O에서는 데이터가 커널 버퍼 → 유저스페이스 데몬 버퍼 → 커널 버퍼 경로를 거치며 최소 2번의 메모리 복사가 발생합니다. splice 기반 zero-copy는 커널 파이프를 중간 버퍼로 활용하여 이 복사를 제거합니다.
FUSE_BUF_SPLICE 플래그
libfuse3의 버퍼 플래그를 통해 splice 동작을 세밀하게 제어할 수 있습니다:
/* libfuse3 버퍼 플래그 */
enum fuse_buf_flags {
FUSE_BUF_IS_FD = (1 << 1), /* 버퍼가 fd (파이프 등) */
FUSE_BUF_FD_SEEK = (1 << 2), /* fd에 offset 사용 */
FUSE_BUF_FD_RETRY = (1 << 3), /* 부분 I/O 시 재시도 */
};
enum fuse_buf_copy_flags {
FUSE_BUF_NO_SPLICE = (1 << 1), /* splice 비활성화 */
FUSE_BUF_FORCE_SPLICE = (1 << 2), /* splice 강제 */
FUSE_BUF_SPLICE_MOVE = (1 << 3), /* 페이지 이동 (복사 대신) */
FUSE_BUF_SPLICE_NONBLOCK = (1 << 4), /* non-blocking splice */
};
/* write_buf 콜백 (splice 지원 버전) */
static int myfs_write_buf(const char *path,
struct fuse_bufvec *buf,
off_t offset,
struct fuse_file_info *fi)
{
struct fuse_bufvec dst = FUSE_BUFVEC_INIT(
fuse_buf_size(buf));
dst.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
dst.buf[0].fd = fi->fh;
dst.buf[0].pos = offset;
/* FUSE_BUF_SPLICE_MOVE로 페이지 이동 (zero-copy) */
return fuse_buf_copy(&dst, buf, FUSE_BUF_SPLICE_MOVE);
}
write_buf 콜백(Callback)과 일반 write 콜백을 워크로드에 맞게 선택하세요.
Passthrough 모드 (커널 6.9+)
FUSE passthrough는 데이터 I/O(read/write)를 유저스페이스 데몬을 거치지 않고 커널에서 직접 백엔드(backing) 파일에 수행하는 기능입니다. 메타데이터 연산(lookup, getattr, open 등)만 데몬이 처리하고, 실제 데이터 경로는 커널이 네이티브 파일시스템처럼 직접 처리합니다. 이로써 FUSE의 가장 큰 병목인 컨텍스트 스위칭과 데이터 복사를 완전히 제거할 수 있습니다.
passthrough 설정 과정
/* 1단계: FUSE_INIT에서 passthrough 기능 협상 */
static void *myfs_init(struct fuse_conn_info *conn,
struct fuse_config *cfg)
{
/* FUSE_PASSTHROUGH 지원 여부 확인 */
if (conn->capable & FUSE_CAP_PASSTHROUGH) {
conn->want |= FUSE_CAP_PASSTHROUGH;
}
return NULL;
}
/* 2단계: OPEN 응답에서 backing fd 전달 */
static int myfs_open(const char *path,
struct fuse_file_info *fi)
{
char backing_path[PATH_MAX];
snprintf(backing_path, sizeof(backing_path),
"/data/backend%s", path);
int fd = open(backing_path, fi->flags);
if (fd < 0)
return -errno;
fi->fh = fd;
fi->passthrough_fh = fd; /* 커널에 backing fd 전달 */
fi->keep_cache = 1;
return 0;
}
/* 3단계: read/write 콜백은 호출되지 않음!
* 커널이 backing_fd를 통해 직접 I/O 수행.
* 메타데이터 콜백(getattr, setattr 등)만 데몬이 처리. */
io_uring과 passthrough 조합
FUSE passthrough와 io_uring을 함께 사용하면, 애플리케이션은 io_uring을 통해 비동기 I/O를 제출하고 커널이 backing file에 직접 I/O를 수행합니다. 유저-커널 전환이 io_uring의 submission queue를 통해 최소화되므로, 높은 IOPS 워크로드에서 네이티브 파일시스템에 근접한 성능을 달성할 수 있습니다.
libfuse 3.16+가 필요합니다. 또한 backing file의 파일시스템이 반드시 로컬 네이티브 FS(ext4, XFS 등)여야 합니다. 네트워크 파일시스템이나 다른 FUSE 파일시스템을 backing으로 사용할 수 없습니다.
Writeback 캐시 모델
FUSE의 기본 쓰기 모드는 write-through로, 모든 쓰기가 즉시 데몬에 전달됩니다. writeback 캐시를 활성화하면 커널 페이지 캐시가 쓰기를 버퍼링하고, 적절한 시점에 배치로 데몬에 플러시합니다. 이는 소규모 쓰기가 빈번한 워크로드에서 극적인 성능 향상을 가져옵니다.
일관성 모델과 주의사항
writeback 캐시는 성능과 일관성 사이의 트레이드오프입니다:
| 항목 | Write-Through | Writeback Cache |
|---|---|---|
| 쓰기 반영 시점 | 즉시 (동기) | 지연 (비동기 배치) |
| 소규모 쓰기 성능 | 느림 (매번 IPC) | 빠름 (캐시 히트) |
| 크래시 시 데이터 | 안전 (이미 전달) | 유실 가능 (더티 페이지(Dirty Page)) |
| 다중 접근 일관성 | 강한 일관성 | 약한 일관성 (stale read 가능) |
| 파일 크기 추적 | 데몬이 관리 | 커널이 관리 (ATTR_SIZE) |
| 적합한 워크로드 | 데이터 안전 중시 | 성능 중시, 로그 파일 등 |
/* writeback 캐시 활성화 */
static void *myfs_init(struct fuse_conn_info *conn,
struct fuse_config *cfg)
{
if (conn->capable & FUSE_CAP_WRITEBACK_CACHE) {
conn->want |= FUSE_CAP_WRITEBACK_CACHE;
}
/* writeback 모드에서는 커널이 파일 크기를 관리 */
/* 데몬의 getattr 응답에서 st_size가 무시될 수 있음 */
return NULL;
}
/* writeback 모드에서 fsync 처리 */
static int myfs_fsync(const char *path, int datasync,
struct fuse_file_info *fi)
{
/* 커널이 fsync 전에 모든 더티 페이지를 WRITE로 전달
* 데몬은 여기서 백엔드 스토리지에 실제 동기화 수행 */
if (datasync)
return fdatasync(fi->fh);
return fsync(fi->fh);
}
DAX (Direct Access) 모드
FUSE DAX는 호스트 메모리의 파일 데이터를 게스트 VM의 주소 공간(Address Space)에 직접 매핑하는 기술입니다. 데이터 복사 없이 메모리 매핑된 I/O(mmap)가 가능하여, virtiofs 환경에서 네이티브에 가까운 성능을 제공합니다. 이 모드는 주로 virtiofs + QEMU 조합에서 사용됩니다.
FUSE_SETUPMAPPING / FUSE_REMOVEMAPPING
DAX 모드에서 FUSE 커널 모듈은 두 가지 특수 opcode를 사용하여 메모리 매핑을 관리합니다:
/* DAX 매핑 설정 요청 */
struct fuse_setupmapping_in {
uint64_t fh; /* 파일 핸들 */
uint64_t foffset; /* 파일 내 오프셋 */
uint64_t len; /* 매핑 길이 */
uint64_t flags; /* FUSE_SETUPMAPPING_FLAG_WRITE 등 */
uint64_t moffset; /* DAX window 내 오프셋 */
};
/* DAX 매핑 해제 요청 */
struct fuse_removemapping_in {
uint32_t count; /* 해제할 매핑 수 */
};
struct fuse_removemapping_one {
uint64_t moffset; /* DAX window 내 오프셋 */
uint64_t len; /* 매핑 길이 */
};
/* DAX window 크기 설정 (QEMU 측) */
/* -device vhost-user-fs-pci,...,cache-size=1G */
| DAX 파라미터 | 설명 | 권장값 |
|---|---|---|
cache-size | DAX window 크기 (QEMU) | VM 메모리의 25~50% |
--cache=always | virtiofsd 캐시 정책 | DAX 사용 시 필수 |
dax=always | 게스트 마운트 옵션 | 항상 DAX 사용 |
dax=inode | 게스트 마운트 옵션 | 파일별 DAX 선택 (6.2+) |
dax=never | 게스트 마운트 옵션 | DAX 비활성화 |
virtiofs 아키텍처
virtiofs는 FUSE 프로토콜을 virtio 전송 계층 위에서 사용하여 호스트-게스트 간 고성능 파일시스템 공유를 구현합니다. 기존 9p(virtio-9p)보다 성능이 우수하며, FUSE의 풍부한 캐시 정책을 그대로 활용할 수 있습니다.
virtiofs vs 9p(virtio-9p) 비교
| 항목 | virtiofs | 9p (virtio-9p) |
|---|---|---|
| 프로토콜 | FUSE | 9P2000.L |
| 캐시 정책 | none / auto / always / DAX | none / loose / mmap |
| DAX 지원 | 지원 (공유 메모리) | 미지원 |
| 멀티 큐 | 지원 (request VQ 다중) | 단일 큐 |
| POSIX 호환성 | 높음 (FUSE 시맨틱) | 중간 (9P 제약) |
| 순차 읽기 성능 | ~3,000 MB/s (DAX) | ~800 MB/s |
| 메타데이터 성능 | ~80K ops/s | ~20K ops/s |
| 호스트 데몬 | virtiofsd (Rust) | QEMU 내장 |
| 보안 샌드박싱 | chroot / namespace | 제한적 |
멀티 큐와 CPU 친화성
virtiofs는 여러 request virtqueue를 지원합니다. 각 vCPU가 별도의 큐를 사용하면 I/O 병렬성이 크게 향상됩니다:
# virtiofsd: 멀티스레드 설정
/usr/libexec/virtiofsd \
--socket-path=/tmp/virtiofs.sock \
--shared-dir=/shared \
--thread-pool-size=8 # 요청 처리 스레드 수
# QEMU: 멀티 큐 설정
qemu-system-x86_64 \
-chardev socket,id=char0,path=/tmp/virtiofs.sock \
-device vhost-user-fs-pci,chardev=char0,tag=myfs,\
queue-size=1024,num-request-queues=4 \
-object memory-backend-memfd,id=mem,size=8G,share=on \
-numa node,memdev=mem \
...
# 게스트에서 마운트 (DAX 포함)
mount -t virtiofs -o dax=always myfs /mnt/shared
커널 내부 구조
FUSE 커널 모듈은 fs/fuse/ 디렉토리에 구현되어 있으며, 주요 구조체와 요청 처리 흐름을 이해해야 성능 문제를 진단하고 커널 레벨 최적화를 적용할 수 있습니다.
fuse_conn 라이프사이클
/* fs/fuse/inode.c — FUSE 마운트 시 초기화 */
static int fuse_fill_super(struct super_block *sb,
struct fs_context *fsc)
{
struct fuse_conn *fc;
struct fuse_mount *fm;
fc = kzalloc(sizeof(*fc), GFP_KERNEL);
fuse_conn_init(fc, sb->s_user_ns, ...);
/* 기본 설정 */
fc->max_read = FUSE_MAX_PAGES_PER_REQ * PAGE_SIZE;
fc->max_write = FUSE_MAX_PAGES_PER_REQ * PAGE_SIZE;
fc->max_pages = FUSE_DEFAULT_MAX_PAGES_PER_REQ;
fc->max_background = FUSE_DEFAULT_MAX_BACKGROUND;
fc->congestion_threshold = FUSE_DEFAULT_CONGESTION_THRESHOLD;
/* /dev/fuse fd와 연결 */
fm = kzalloc(sizeof(*fm), GFP_KERNEL);
fm->fc = fc;
sb->s_fs_info = fm;
/* FUSE_INIT 전송 → 데몬과 핸드셰이크 */
fuse_send_init(fm);
return 0;
}
/* fuse_conn 참조 카운팅 */
void fuse_conn_get(struct fuse_conn *fc); /* refcount++ */
void fuse_conn_put(struct fuse_conn *fc); /* refcount--, 0이면 해제 */
요청 큐 관리와 배경 요청
/* 동기 요청 전송 */
ssize_t fuse_simple_request(struct fuse_mount *fm,
struct fuse_args *args)
{
struct fuse_req *req;
/* 1. 요청 할당 및 초기화 */
req = fuse_get_req(fm, 0);
/* 2. 인자 설정 */
fuse_args_to_req(req, args);
/* 3. fuse_iqueue에 큐잉 */
__fuse_request_send(req);
/* 4. 응답 대기 (sleep) */
wait_event(req->waitq, req->state == FUSE_REQ_REPLIED);
/* 5. 결과 반환 */
return req->out.h.error;
}
/* 비동기 배경 요청 (writeback 등) */
ssize_t fuse_simple_background(struct fuse_mount *fm,
struct fuse_args *args,
gfp_t gfp_flags)
{
/* max_background 제한 확인 */
if (fc->num_background >= fc->max_background)
wait_event(fc->blocked_waitq, ...);
fc->num_background++;
if (fc->num_background >= fc->congestion_threshold)
set_bdi_congested(fc->bdi, ...); /* BDI 혼잡 표시 */
/* 큐에 넣고 즉시 반환 */
__fuse_request_send(req);
return 0;
}
fuse_inode와 nodeid 관리
FUSE는 VFS의 inode 구조체를 확장한 fuse_inode를 사용합니다. nodeid는 유저스페이스 데몬과 커널 사이에서 파일을 식별하는 핵심 필드입니다:
/* fs/fuse/fuse_i.h */
struct fuse_inode {
struct inode inode; /* VFS inode (내장) */
u64 nodeid; /* FUSE 프로토콜 노드 ID */
u64 nlookup; /* lookup 참조 카운트 */
struct fuse_forget_link *forget; /* forget 요청 연결 */
u64 attr_version; /* 속성 버전 (캐시 검증) */
union {
struct {
ktime_t attr_valid; /* 속성 캐시 만료 시각 */
};
struct rcu_head rcu;
};
struct mutex mutex; /* 직렬화 (parallel_dirops 비활성 시) */
spinlock_t lock; /* inode 상태 보호 */
};
/* nodeid ↔ inode 매핑 */
static inline u64 get_node_id(struct inode *inode) {
return get_fuse_inode(inode)->nodeid;
}
/* 루트 inode의 nodeid는 항상 FUSE_ROOT_ID (1) */
FUSE 구현체 비교 분석
다양한 FUSE 기반 파일시스템의 구현 특성을 비교하여, 프로젝트 요구사항에 맞는 구현체를 선택할 수 있도록 합니다. 아래 다이어그램은 주요 구현체의 아키텍처 분류를 보여줍니다.
| 구현체 | 언어 | API 레벨 | 멀티스레드 | splice | writeback | passthrough | 주요 용도 |
|---|---|---|---|---|---|---|---|
| SSHFS | C | High-level | 지원 | 지원 | 지원 | 미지원 | SSH 원격 마운트 |
| GlusterFS | C | Low-level | 지원 | 지원 | 부분 | 미지원 | 분산 스토리지 |
| s3fs-fuse | C++ | High-level | 지원 | 미지원 | 지원 | 미지원 | S3 객체 스토리지 |
| NTFS-3G | C | Low-level | 미지원 | 미지원 | 미지원 | 미지원 | NTFS 읽기/쓰기 |
| ceph-fuse | C++ | Low-level | 지원 | 미지원 | 지원 | 미지원 | 분산 FS 클라이언트 |
| rclone mount | Go | cgofuse | 지원 | 미지원 | 지원 | 미지원 | 클라우드 스토리지 |
| gocryptfs | Go | go-fuse | 지원 | 미지원 | 미지원 | 미지원 | 파일 암호화 |
| mergerfs | C++ | Low-level | 지원 | 지원 | 미지원 | 지원(6.9+) | 디스크 풀링 |
| CurlFtpFS | C | High-level | 미지원 | 미지원 | 미지원 | 미지원 | FTP 마운트 |
| virtiofsd | Rust | 전용 | 지원 | N/A | 지원 | 지원 | VM 파일 공유 |
언어별 FUSE 바인딩
| 언어 | 라이브러리 | API 유형 | 특징 |
|---|---|---|---|
| C | libfuse3 | High/Low-level | 공식 레퍼런스 구현, 가장 완전한 기능 |
| Rust | fuse3 / fuser | Low-level | 메모리 안전성, async/await 지원 |
| Go | go-fuse / cgofuse | Low-level | goroutine 기반 병렬 처리 |
| Python | pyfuse3 / fusepy | High-level | 프로토타이핑에 적합, 성능 제약 |
| Java | jnr-fuse | High-level | JNI 기반, JVM 생태계 활용 |
| C++ | libfuse3 (직접) | High/Low-level | C API 직접 사용, RAII 래퍼 구현 |
구현체 선택 가이드
- 프로토타이핑/학습 — Python (pyfuse3)으로 빠르게 시작, 이후 C/Rust로 이식
- 고성능 요구 — C (libfuse3 low-level) 또는 Rust (fuser), splice + writeback 활용
- 분산 시스템 — C/C++ (libfuse3 low-level) + 멀티스레드 + 비동기 I/O
- 클라우드 통합 — Go (go-fuse) + 클라우드 SDK, goroutine 활용
- VM 환경 — virtiofsd (Rust) + passthrough + DAX
ftrace/bpftrace FUSE 성능 분석
FUSE 성능 문제를 진단할 때, ftrace와 bpftrace를 활용하면 커널 내부 요청 흐름과 지연 시간을 정밀하게 측정할 수 있습니다. 아래 다이어그램은 FUSE 요청 파이프라인(Pipeline)에서 추적 가능한 지점을 보여줍니다.
ftrace로 FUSE 요청 추적
# FUSE 관련 함수 추적 활성화
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'fuse_*' > /sys/kernel/debug/tracing/set_ftrace_filter
# 필터 확인
cat /sys/kernel/debug/tracing/set_ftrace_filter
# fuse_simple_request
# fuse_dev_read
# fuse_dev_write
# fuse_readpages
# ...
# 추적 시작
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 테스트 워크로드 실행
cat /mnt/fuse/test_file > /dev/null
# 추적 결과 확인
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
# 결과 예시:
# 0) | fuse_simple_request() {
# 0) | __fuse_request_send() {
# 0) 0.850 us | fuse_queue_iqueue();
# 0) | wait_event() {
# 0) ! 125.3 us | }
# 0) ! 127.1 us | }
# 0) ! 128.5 us | }
bpftrace로 FUSE 지연시간 히스토그램
# FUSE 요청별 지연시간 분포 (히스토그램)
bpftrace -e '
kprobe:fuse_simple_request {
@start[tid] = nsecs;
}
kretprobe:fuse_simple_request /@start[tid]/ {
@usecs = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
END { print(@usecs); }
'
# 출력 예시:
# @usecs:
# [4, 8) 12 |@@ |
# [8, 16) 45 |@@@@@@@ |
# [16, 32) 189 |@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
# [32, 64) 234 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
# [64, 128) 87 |@@@@@@@@@@@@ |
# [128, 256) 23 |@@@ |
# [256, 512) 5 | |
opcode별 지연시간 추적
# FUSE opcode별 평균 지연시간 추적
bpftrace -e '
#include <linux/fuse.h>
kprobe:fuse_dev_do_read {
@start[tid] = nsecs;
}
kprobe:fuse_dev_do_write {
@req_start[arg2] = nsecs; /* unique ID 기준 */
}
kretprobe:fuse_dev_do_write /@req_start[arg2]/ {
$lat = (nsecs - @req_start[arg2]) / 1000;
@op_latency[arg3] = avg($lat); /* opcode별 평균 */
delete(@req_start[arg2]);
}
'
# /dev/fuse read/write 사이의 시간을 직접 측정하는 대안
bpftrace -e '
tracepoint:syscalls:sys_enter_read /args->fd == FUSE_FD/ {
@read_start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read /@read_start[tid]/ {
@daemon_read_us = hist((nsecs - @read_start[tid]) / 1000);
delete(@read_start[tid]);
}
'
/proc 및 /sys 카운터 활용
# FUSE 연결 상태 모니터링 스크립트
for conn in /sys/fs/fuse/connections/*/; do
echo "=== Connection: $(basename $conn) ==="
echo " Waiting: $(cat ${conn}waiting)"
echo " Max BG: $(cat ${conn}max_background)"
echo " Congestion: $(cat ${conn}congestion_threshold)"
done
# vmstat으로 컨텍스트 스위칭 모니터링
vmstat 1 | awk '{print $12, $13}' # cs (context switch) 열
# perf로 FUSE 관련 컨텍스트 스위칭 비용
perf stat -e context-switches,cpu-migrations \
-p $(pgrep my_fuse_daemon) -- sleep 10
waiting수치가 지속 증가 → 데몬 처리 속도 부족, 스레드 수 증가 필요fuse_simple_request지연이 100us+ → 데몬 응답 지연, splice/passthrough 검토- 컨텍스트 스위칭이 초당 10만+ → passthrough 모드 전환 검토
max_background도달 빈번 → 값 증가 또는 배치 처리 최적화
성능 분석과 튜닝
FUSE 파일시스템의 성능은 아키텍처 특성상 네이티브 파일시스템보다 제한적이지만, 적절한 튜닝으로 상당한 성능 향상을 달성할 수 있습니다. 이 섹션에서는 병목 분석 방법론과 구체적인 튜닝 파라미터를 다룹니다.
튜닝 파라미터 상세
| 파라미터 | 기본값 | 권장 범위 | 효과 | 주의사항 |
|---|---|---|---|---|
max_read | 128KB | 128KB~1MB | 대용량 순차 읽기 처리량(Throughput) 향상 | 데몬 메모리 사용량 증가 |
max_write | 128KB | 128KB~1MB | 대용량 순차 쓰기 처리량 향상 | 데몬 메모리 사용량 증가 |
max_pages | 32 | 32~256 | 단일 요청 데이터 크기 증가 | 커널 메모리 사용량 증가 |
max_background | 12 | 12~256 | 동시 배경 요청 수 증가 | 데몬 부하 증가 가능 |
congestion_threshold | 9 | max_background의 75% | BDI 혼잡 시작점 조절 | 너무 높으면 메모리 압박 |
attr_timeout | 1.0초 | 1~3600초 | getattr 호출 빈도 감소 | 속성 변경 지연 반영 |
entry_timeout | 1.0초 | 1~3600초 | lookup 호출 빈도 감소 | 이름 변경 지연 반영 |
negative_timeout | 0초 | 0~60초 | 존재하지 않는 파일 캐시 | 새 파일 생성 시 지연 감지 |
max_idle_threads | 10 | CPU 코어 수 | 멀티스레드 처리 능력 | 유휴 스레드 메모리 소비 |
성능 측정 방법
# fio로 FUSE 파일시스템 벤치마크
fio --name=fuse_seq_read \
--directory=/mnt/fuse \
--rw=read \
--bs=128k \
--size=1G \
--numjobs=4 \
--group_reporting
# 랜덤 4K IOPS 측정
fio --name=fuse_rand_read \
--directory=/mnt/fuse \
--rw=randread \
--bs=4k \
--size=256M \
--numjobs=4 \
--iodepth=32 \
--group_reporting
# 메타데이터 성능 (mdtest)
mdtest -d /mnt/fuse/test -n 10000 -i 3
# FUSE 오버헤드 비교 (동일 백엔드에서 native vs FUSE)
# 1. native ext4에서 fio 실행
fio --name=native --directory=/data/backend --rw=read --bs=128k --size=1G
# 2. 같은 backend를 사용하는 FUSE에서 fio 실행
fio --name=fuse --directory=/mnt/fuse --rw=read --bs=128k --size=1G
# 3. 차이가 FUSE 오버헤드
최적화 적용 체크리스트
- 캐시 타임아웃 조정 (가장 간단) —
attr_timeout,entry_timeout을 워크로드에 맞게 증가 - writeback 캐시 활성화 — 쓰기 워크로드 성능 3~5배 향상
- splice 활성화 —
write_buf콜백 구현으로 대용량 I/O 최적화 - 멀티스레드 —
clone_fd=1, 적절한max_idle_threads설정 - max_background 증가 — 높은 병렬성 워크로드에서 배경 요청 제한 완화
- READDIRPLUS 활성화 —
ls -l패턴의 메타데이터 성능 향상 - passthrough 전환 (최대 효과, 6.9+ 필요) — 데이터 I/O를 커널이 직접 처리
FUSE Hello World 완전 예제
FUSE 파일시스템 개발의 첫걸음은 최소한의 연산만 구현한 "Hello World" 파일시스템입니다. 이 섹션에서는 getattr, readdir, open, read 4개 콜백만으로 동작하는 읽기 전용 파일시스템을 처음부터 끝까지 구현합니다.
최소 FUSE 파일시스템 구현
아래 코드는 마운트 포인트에 hello.txt 파일 하나만 존재하는 가장 단순한 FUSE 파일시스템입니다. libfuse3의 high-level API를 사용합니다:
/* hello_fuse.c — 최소 FUSE 파일시스템 */
#define FUSE_USE_VERSION 31
#include <fuse3/fuse.h>
#include <string.h>
#include <errno.h>
#include <stddef.h>
static const char *hello_path = "/hello.txt";
static const char *hello_str = "Hello, FUSE!\n";
/* getattr: stat() 요청 처리 — 파일/디렉토리 속성 반환 */
static int hello_getattr(const char *path,
struct stat *stbuf,
struct fuse_file_info *fi)
{
(void)fi;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
return 0;
}
if (strcmp(path, hello_path) == 0) {
stbuf->st_mode = S_IFREG | 0444;
stbuf->st_nlink = 1;
stbuf->st_size = (off_t)strlen(hello_str);
return 0;
}
return -ENOENT;
}
/* readdir: 디렉토리 목록 요청 — 루트에 hello.txt 하나 반환 */
static int hello_readdir(const char *path, void *buf,
fuse_fill_dir_t filler,
off_t offset,
struct fuse_file_info *fi,
enum fuse_readdir_flags flags)
{
(void)offset; (void)fi; (void)flags;
if (strcmp(path, "/") != 0)
return -ENOENT;
filler(buf, ".", NULL, 0, 0);
filler(buf, "..", NULL, 0, 0);
filler(buf, "hello.txt", NULL, 0, 0);
return 0;
}
/* open: 파일 열기 — 읽기 전용만 허용 */
static int hello_open(const char *path,
struct fuse_file_info *fi)
{
if (strcmp(path, hello_path) != 0)
return -ENOENT;
if ((fi->flags & O_ACCMODE) != O_RDONLY)
return -EACCES;
return 0;
}
/* read: 파일 데이터 읽기 — offset/size 범위 반환 */
static int hello_read(const char *path, char *buf,
size_t size, off_t offset,
struct fuse_file_info *fi)
{
size_t len;
(void)fi;
if (strcmp(path, hello_path) != 0)
return -ENOENT;
len = strlen(hello_str);
if ((size_t)offset >= len)
return 0;
if (offset + size > len)
size = len - offset;
memcpy(buf, hello_str + offset, size);
return (int)size;
}
static const struct fuse_operations hello_ops = {
.getattr = hello_getattr,
.readdir = hello_readdir,
.open = hello_open,
.read = hello_read,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &hello_ops, NULL);
}
Makefile
# Makefile — libfuse3 기반 FUSE 파일시스템 빌드
CC = gcc
CFLAGS = -Wall -Wextra -O2 $(shell pkg-config --cflags fuse3)
LDFLAGS = $(shell pkg-config --libs fuse3)
hello_fuse: hello_fuse.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
clean:
rm -f hello_fuse
# 의존성 설치 (Debian/Ubuntu)
# sudo apt install libfuse3-dev pkg-config
# Fedora: sudo dnf install fuse3-devel
마운트/언마운트 실행
# 빌드
make hello_fuse
# 마운트 포인트 생성
mkdir -p /tmp/hello_mount
# 포그라운드 실행 (디버그 출력 포함)
./hello_fuse -f -d /tmp/hello_mount
# 다른 터미널에서 확인
ls -la /tmp/hello_mount/
# drwxr-xr-x 2 user user 0 ... .
# -r--r--r-- 1 user user 13 ... hello.txt
cat /tmp/hello_mount/hello.txt
# Hello, FUSE!
# 언마운트
fusermount3 -u /tmp/hello_mount
# 또는 root로
umount /tmp/hello_mount
strace로 FUSE 요청 관찰
FUSE 데몬의 /dev/fuse 통신을 strace로 추적하면 프로토콜 메시지를 직접 관찰할 수 있습니다:
# FUSE 데몬 프로세스에 strace 연결
strace -f -e trace=read,write -p $(pidof hello_fuse) 2>&1 | head -50
# 출력 예시: ls /tmp/hello_mount 수행 시
# read(3, "\x...", 262144) = 56 ← FUSE_LOOKUP 요청 수신
# write(3, "\x...", 144) = 144 ← fuse_entry_out 응답 전송
# read(3, "\x...", 262144) = 40 ← FUSE_OPENDIR 요청
# write(3, "\x...", 24) = 24 ← fuse_open_out 응답
# read(3, "\x...", 262144) = 48 ← FUSE_READDIR 요청
# write(3, "\x...", 80) = 80 ← 디렉토리 엔트리 응답
# /dev/fuse fd 번호 확인
ls -la /proc/$(pidof hello_fuse)/fd/ | grep fuse
# lr-x------ 1 user user 64 ... 3 -> /dev/fuse
# fusectl로 연결 상태 확인
mount -t fusectl fusectl /sys/fs/fuse/connections
ls /sys/fs/fuse/connections/
# 42/ ← 연결 번호
cat /sys/fs/fuse/connections/42/waiting
# 0 ← 대기 중인 요청 수
-d 플래그로 실행하면 libfuse가 모든 요청/응답을 stderr에 출력합니다. 프로덕션에서는 -f(포그라운드)만 사용하여 로그를 journald로 캡처하세요.
Low-level API 실전
libfuse는 두 가지 API를 제공합니다. High-level API는 경로(const char *path) 기반으로 동작하여 단순하지만, Low-level API는 inode 번호(fuse_ino_t) 기반으로 동작하여 커널의 dentry/inode 캐시와 직접 상호작용합니다. 고성능 파일시스템은 대부분 low-level API를 사용합니다.
fuse_lowlevel_ops 구현
Low-level API의 핵심은 lookup, forget, getattr 콜백을 inode 번호 기반으로 구현하는 것입니다:
/* low-level FUSE 파일시스템 — inode 기반 구현 */
#define FUSE_USE_VERSION 34
#include <fuse_lowlevel.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#define HELLO_INO 2 /* inode 1 = root */
static const char *hello_name = "hello.txt";
static const char *hello_data = "Hello from low-level FUSE!\n";
/* lookup: 부모 inode + 이름 → 자식 inode 조회 */
static void ll_lookup(fuse_req_t req, fuse_ino_t parent,
const char *name)
{
struct fuse_entry_param e;
if (parent != FUSE_ROOT_ID || strcmp(name, hello_name) != 0) {
fuse_reply_err(req, ENOENT);
return;
}
memset(&e, 0, sizeof(e));
e.ino = HELLO_INO;
e.attr_timeout = 1.0; /* 속성 캐시 1초 */
e.entry_timeout = 1.0; /* 엔트리 캐시 1초 */
e.attr.st_ino = HELLO_INO;
e.attr.st_mode = S_IFREG | 0444;
e.attr.st_nlink = 1;
e.attr.st_size = strlen(hello_data);
fuse_reply_entry(req, &e);
/* 커널이 nlookup++; forget에서 nlookup-- */
}
/* forget: 커널이 inode 캐시에서 제거할 때 호출 */
static void ll_forget(fuse_req_t req, fuse_ino_t ino,
uint64_t nlookup)
{
(void)ino; (void)nlookup;
/* 실제 FS: nlookup 카운터 감소, 0이면 자원 해제 */
fuse_reply_none(req);
}
/* getattr: inode 번호로 속성 조회 */
static void ll_getattr(fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi)
{
struct stat stbuf;
(void)fi;
memset(&stbuf, 0, sizeof(stbuf));
if (ino == FUSE_ROOT_ID) {
stbuf.st_ino = FUSE_ROOT_ID;
stbuf.st_mode = S_IFDIR | 0755;
stbuf.st_nlink = 2;
} else if (ino == HELLO_INO) {
stbuf.st_ino = HELLO_INO;
stbuf.st_mode = S_IFREG | 0444;
stbuf.st_nlink = 1;
stbuf.st_size = strlen(hello_data);
} else {
fuse_reply_err(req, ENOENT);
return;
}
fuse_reply_attr(req, &stbuf, 1.0);
}
/* read: inode 기반 데이터 읽기 */
static void ll_read(fuse_req_t req, fuse_ino_t ino,
size_t size, off_t off,
struct fuse_file_info *fi)
{
(void)fi;
if (ino != HELLO_INO) {
fuse_reply_err(req, EISDIR);
return;
}
size_t len = strlen(hello_data);
if ((size_t)off >= len)
fuse_reply_buf(req, NULL, 0);
else
fuse_reply_buf(req, hello_data + off,
(off + size > len) ? len - off : size);
}
static const struct fuse_lowlevel_ops ll_ops = {
.lookup = ll_lookup,
.forget = ll_forget,
.getattr = ll_getattr,
.read = ll_read,
};
fuse_reply_* 응답 패턴
Low-level API에서는 모든 콜백이 fuse_reply_* 함수로 명시적 응답을 보내야 합니다. 응답하지 않으면 커널 측 프로세스가 영구히 블록됩니다:
/* fuse_reply_* 함수 패턴 정리 */
/* 1. 성공 응답 */
fuse_reply_entry(req, &entry_param); /* lookup 성공 */
fuse_reply_attr(req, &stat, timeout); /* getattr 성공 */
fuse_reply_open(req, &fi); /* open 성공 */
fuse_reply_buf(req, buf, size); /* read 데이터 */
fuse_reply_write(req, count); /* write 바이트 수 */
fuse_reply_create(req, &entry, &fi); /* create 성공 */
fuse_reply_statfs(req, &statvfs); /* statfs 결과 */
/* 2. 에러 응답 */
fuse_reply_err(req, ENOENT); /* 파일 없음 */
fuse_reply_err(req, EACCES); /* 권한 거부 */
fuse_reply_err(req, EIO); /* I/O 에러 */
fuse_reply_err(req, 0); /* 성공 (unlink, rmdir 등) */
/* 3. 특수 응답 */
fuse_reply_none(req); /* forget 전용 (응답 없음) */
fuse_reply_xattr(req, size); /* xattr 크기만 반환 */
fuse_reply_readlink(req, linkpath); /* 심볼릭 링크 대상 */
/* 4. iov 기반 zero-copy 응답 */
struct iovec iov[2] = {
{ .iov_base = header, .iov_len = hdr_len },
{ .iov_base = data, .iov_len = data_len },
};
fuse_reply_iov(req, iov, 2); /* scatter 응답 */
/* 주의: 모든 콜백에서 반드시 하나의 reply를 보내야 함 */
/* 이중 reply → 크래시, reply 누락 → 커널 프로세스 영구 블록 */
lookup 성공 시 커널이 nlookup을 증가시키고, forget에서 감소시킵니다. 데몬이 종료되면 커널이 FUSE_DESTROY 후 남은 모든 inode에 대해 forget을 호출합니다. nlookup이 0이 되면 해당 inode의 자원을 해제해야 합니다.
멀티스레드/비동기 FUSE
FUSE 데몬의 성능을 극대화하려면 단일 스레드 이벤트 루프에서 벗어나 멀티스레드 또는 비동기 I/O 모델을 사용해야 합니다. libfuse3는 fuse_session_loop_mt를 통해 멀티스레드 모드를 기본 지원하며, 커널 5.x 이후에는 io_uring 기반 FUSE도 논의되고 있습니다.
fuse_session_loop_mt 구조
/* Low-level API 멀티스레드 이벤트 루프 설정 */
#define FUSE_USE_VERSION 34
#include <fuse_lowlevel.h>
int main(int argc, char *argv[])
{
struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
struct fuse_session *se;
struct fuse_cmdline_opts opts;
struct fuse_loop_config config;
int ret = -1;
if (fuse_parse_cmdline(&args, &opts) != 0)
return 1;
se = fuse_session_new(&args, &ll_ops,
sizeof(ll_ops), NULL);
if (!se) goto err_out;
if (fuse_set_signal_handlers(se) != 0)
goto err_session;
if (fuse_session_mount(se, opts.mountpoint) != 0)
goto err_signal;
/* 멀티스레드 루프 설정 */
memset(&config, 0, sizeof(config));
config.clone_fd = 1; /* 스레드별 /dev/fuse fd 복제 */
config.max_idle_threads = 10; /* 유휴 스레드 최대 수 */
if (opts.singlethread)
ret = fuse_session_loop(se);
else
ret = fuse_session_loop_mt(se, &config);
fuse_session_unmount(se);
err_signal:
fuse_remove_signal_handlers(se);
err_session:
fuse_session_destroy(se);
err_out:
free(opts.mountpoint);
fuse_opt_free_args(&args);
return ret ? 1 : 0;
}
clone_fd=1이면 각 워커 스레드가 FUSE_DEV_IOC_CLONE ioctl로 /dev/fuse fd를 복제합니다. 이렇게 하면 스레드가 독립적으로 read()를 호출하여 커널 큐에서 요청을 가져오므로, 단일 fd에 대한 경합 없이 병렬 처리가 가능합니다.
splice 기반 zero-copy 전송
/* write_buf 콜백: splice로 zero-copy 쓰기 지원 */
static int myfs_write_buf(const char *path,
struct fuse_bufvec *in_buf,
off_t off,
struct fuse_file_info *fi)
{
struct fuse_bufvec out_buf = FUSE_BUFVEC_INIT(
fuse_buf_size(in_buf));
int backing_fd = (int)fi->fh;
/* splice로 커널→유저 복사 없이 직접 backing fd로 전송 */
out_buf.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
out_buf.buf[0].fd = backing_fd;
out_buf.buf[0].pos = off;
return fuse_buf_copy(&out_buf, in_buf, FUSE_BUF_SPLICE_NONBLOCK);
/* 커널이 splice()를 사용하여 파이프 경유 zero-copy 전송 */
/* in_buf: /dev/fuse fd → pipe → backing fd */
}
/* read_buf 콜백: zero-copy 읽기 */
static int myfs_read_buf(const char *path,
struct fuse_bufvec **bufp,
size_t size, off_t off,
struct fuse_file_info *fi)
{
struct fuse_bufvec *src;
src = malloc(sizeof(*src));
*src = FUSE_BUFVEC_INIT(size);
/* backing fd에서 직접 splice로 전송 */
src->buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
src->buf[0].fd = (int)fi->fh;
src->buf[0].pos = off;
*bufp = src;
return 0;
}
io_uring FUSE (실험적)
커널 6.x에서 io_uring을 통한 FUSE 요청 처리가 실험적으로 가능합니다. 기존 read()/write() 시스템 콜 대신 io_uring의 비동기 SQE/CQE를 사용하면 시스템 콜 오버헤드를 줄일 수 있습니다:
/* io_uring 기반 FUSE 요청 처리 (개념적 구현) */
#include <liburing.h>
struct fuse_uring_ctx {
struct io_uring ring;
int fuse_fd;
char *req_bufs[64]; /* 미리 할당된 요청 버퍼 */
};
static int fuse_uring_init(struct fuse_uring_ctx *ctx,
int fuse_fd)
{
struct io_uring_params params = {};
int ret;
ctx->fuse_fd = fuse_fd;
ret = io_uring_queue_init_params(64, &ctx->ring, ¶ms);
if (ret < 0) return ret;
/* 요청 버퍼 사전 할당 및 등록 */
for (int i = 0; i < 64; i++) {
ctx->req_bufs[i] = aligned_alloc(4096, 262144);
/* FUSE max_write 기본값 = 256KB */
}
/* 고정 버퍼 등록으로 복사 최소화 */
struct iovec iovs[64];
for (int i = 0; i < 64; i++) {
iovs[i].iov_base = ctx->req_bufs[i];
iovs[i].iov_len = 262144;
}
io_uring_register_buffers(&ctx->ring, iovs, 64);
return 0;
}
/* SQE 제출: /dev/fuse에서 비동기 read */
static void submit_fuse_read(struct fuse_uring_ctx *ctx,
int buf_idx)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(&ctx->ring);
io_uring_prep_read_fixed(sqe, ctx->fuse_fd,
ctx->req_bufs[buf_idx], 262144, 0, buf_idx);
io_uring_sqe_set_data64(sqe, buf_idx);
io_uring_submit(&ctx->ring);
}
FUSE Notify 메커니즘
일반적인 FUSE 통신은 커널 → 데몬 방향(요청/응답)이지만, Notify는 데몬 → 커널 방향으로 캐시 무효화 등의 알림을 보냅니다. 원격 파일시스템에서 다른 클라이언트가 파일을 변경했을 때, 로컬 커널 캐시를 즉시 갱신하는 데 핵심적인 역할을 합니다.
fuse_lowlevel_notify_inval_inode/entry
/* 캐시 무효화 Notify 사용 예 */
#include <fuse_lowlevel.h>
/* inode 데이터 캐시 무효화 — 원격 파일 변경 시 */
static void invalidate_file_cache(struct fuse_session *se,
fuse_ino_t ino)
{
int err;
/* 특정 범위만 무효화 (off=0, len=-1은 전체) */
err = fuse_lowlevel_notify_inval_inode(se, ino, 0, -1);
if (err == -ENOENT)
; /* 커널이 이 inode를 캐시하지 않고 있음 — 정상 */
else if (err)
fprintf(stderr, "inval_inode failed: %s\n",
strerror(-err));
/* 부분 무효화: offset 4096부터 8192바이트만 */
fuse_lowlevel_notify_inval_inode(se, ino, 4096, 8192);
}
/* dentry 캐시 무효화 — 파일 이름 변경/삭제 시 */
static void invalidate_entry(struct fuse_session *se,
fuse_ino_t parent,
const char *name)
{
int err;
err = fuse_lowlevel_notify_inval_entry(se, parent,
name, strlen(name));
if (err && err != -ENOENT)
fprintf(stderr, "inval_entry(%s) failed: %s\n",
name, strerror(-err));
}
/* 전형적 사용 패턴: 원격 변경 감지 스레드 */
static void *remote_watcher(void *arg)
{
struct myfs_ctx *ctx = arg;
while (!fuse_session_exited(ctx->se)) {
struct change_event ev;
if (poll_remote_changes(ctx->remote, &ev) > 0) {
if (ev.type == CHANGE_DATA)
invalidate_file_cache(ctx->se, ev.ino);
else if (ev.type == CHANGE_RENAME)
invalidate_entry(ctx->se, ev.parent, ev.name);
}
}
return NULL;
}
fuse_lowlevel_notify_store
notify_store는 데몬이 커널 페이지 캐시에 데이터를 직접 밀어 넣는 기능입니다. 원격에서 prefetch한 데이터를 미리 캐시에 채워둘 때 유용합니다:
/* 커널 페이지 캐시에 데이터 직접 저장 */
static int push_to_kernel_cache(struct fuse_session *se,
fuse_ino_t ino,
off_t offset,
const char *data,
size_t len)
{
struct fuse_bufvec bufv = FUSE_BUFVEC_INIT(len);
bufv.buf[0].mem = (void *)data;
int err = fuse_lowlevel_notify_store(se, ino, offset,
&bufv, 0);
if (err == -ENOENT)
return 0; /* inode가 커널에 없음 — 무시 */
return err;
}
/* 사용 예: readahead 최적화 */
static void prefetch_adjacent(struct myfs_ctx *ctx,
fuse_ino_t ino,
off_t offset)
{
char buf[131072]; /* 128KB */
ssize_t n;
/* 다음 128KB를 원격에서 미리 읽기 */
n = remote_read(ctx->remote, ino, buf,
sizeof(buf), offset + 131072);
if (n > 0)
push_to_kernel_cache(ctx->se, ino,
offset + 131072, buf, n);
}
fuse_operations)에서는 사용할 수 없으며, fuse_session 포인터가 필요합니다. 따라서 캐시 일관성(Cache Coherency)이 중요한 원격/분산 파일시스템은 반드시 Low-level API를 사용해야 합니다.
Passthrough 모드 구현
FUSE passthrough는 커널 6.9에서 도입된 기능으로, 데이터 I/O를 FUSE 데몬을 거치지 않고 커널이 backing 파일에 직접 수행합니다. 메타데이터 연산(open, lookup 등)만 데몬이 처리하고, read()/write()는 커널이 backing fd를 통해 직접 처리하므로 네이티브에 가까운 성능을 달성합니다.
FUSE_PASSTHROUGH ioctl 설정
/* FUSE passthrough 설정 — 커널 6.9+ */
#include <linux/fuse.h>
#include <sys/ioctl.h>
/* FUSE_INIT에서 passthrough 기능 협상 */
static void init_passthrough(void *userdata,
struct fuse_conn_info *conn)
{
/* FUSE_PASSTHROUGH 기능 플래그 요청 */
if (conn->capable & FUSE_CAP_PASSTHROUGH)
conn->want |= FUSE_CAP_PASSTHROUGH;
}
/* open 콜백에서 backing fd 설정 */
static void ll_open(fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi)
{
int backing_fd;
const char *real_path;
/* inode를 실제 backing 경로로 변환 */
real_path = inode_to_path(ino);
backing_fd = open(real_path, fi->flags);
if (backing_fd < 0) {
fuse_reply_err(req, errno);
return;
}
fi->fh = backing_fd;
/* passthrough 설정: 이 fd로 커널이 직접 I/O */
fi->passthrough_fh = backing_fd;
fi->keep_cache = 1;
fuse_reply_open(req, fi);
/* 이후 read/write는 커널이 backing_fd를 통해 직접 수행 */
/* 데몬의 read/write 콜백은 호출되지 않음 */
}
커널 설정 및 마운트
# 커널 설정 확인 (6.9+ 필요)
grep CONFIG_FUSE_PASSTHROUGH /boot/config-$(uname -r)
# CONFIG_FUSE_PASSTHROUGH=y
# passthrough 지원 확인
cat /sys/fs/fuse/features/passthrough
# 1 (지원됨)
# passthrough 모드로 마운트 (데몬이 지원해야 함)
./myfs_passthrough -o allow_other /mnt/fuse /data/backend
# passthrough 동작 확인: strace에서 read/write가 안 보여야 함
strace -f -e trace=read,write -p $(pidof myfs_passthrough) &
dd if=/mnt/fuse/testfile of=/dev/null bs=1M count=100
# 데몬 strace에 read/write 없음 → passthrough 동작 확인
# 성능 비교
# 일반 FUSE: ~800 MB/s (context switch + 메모리 복사)
# Passthrough: ~3,500 MB/s (네이티브에 근접)
Android FUSE Passthrough
Android는 FUSE passthrough의 가장 큰 사용처입니다. 미디어 파일 접근 시 권한 검사는 MediaProvider(FUSE 데몬)가 수행하고, 실제 I/O는 passthrough로 처리합니다:
# Android FUSE 마운트 확인
adb shell mount | grep fuse
# /dev/fuse on /storage/emulated type fuse (rw,nosuid,nodev,...,
# user_id=0,group_id=0,default_permissions,allow_other)
# passthrough 통계 (Android 14+)
adb shell cat /sys/fs/fuse/connections/*/passthrough_count
# 1247 ← passthrough로 처리된 open 수
# MediaProvider FUSE 데몬 확인
adb shell ps -ef | grep -i mediaprovider
# system 1234 ... com.android.providers.media.module
# passthrough로 인한 성능 향상 측정
# 카메라 버스트 촬영: 30fps → 60fps (I/O 지연 감소)
# 4K 비디오 재생: 버퍼 언더런 0% (직접 mmap 가능)
read/write/mmap 콜백이 호출되지 않습니다. 데이터 변환(암호화, 압축 등)이 필요한 파일시스템에서는 사용할 수 없습니다. 또한 backing 파일이 로컬 파일시스템에 있어야 합니다.
virtiofs 구현
virtiofs는 호스트-게스트 파일 공유를 위한 FUSE 기반 프로토콜로, virtio 전송 계층을 사용합니다. DAX(Direct Access) 모드에서는 게스트가 공유 메모리 영역을 통해 호스트 페이지 캐시를 직접 mmap하여 데이터 복사 없이 접근합니다.
DAX window 설정
# virtiofsd (Rust 구현) 설정 — DAX 지원
/usr/libexec/virtiofsd \
--socket-path=/tmp/vfsd.sock \
--shared-dir=/export/shared \
--cache=always \
--sandbox=chroot \
--thread-pool-size=8 \
--announce-submounts \
--inode-file-handles=mandatory
# QEMU DAX window 설정
qemu-system-x86_64 \
-m 8G \
-object memory-backend-memfd,id=mem,size=8G,share=on \
-numa node,memdev=mem \
-chardev socket,id=vfsd,path=/tmp/vfsd.sock \
-device vhost-user-fs-pci,chardev=vfsd,tag=myshare,\
queue-size=1024,num-request-queues=4,\
cache-size=2G \
# cache-size: DAX window 크기 (게스트가 mmap하는 영역)
...
# 게스트에서 마운트
mount -t virtiofs myshare /mnt/shared -o dax=always
# DAX 동작 확인
cat /proc/mounts | grep virtiofs
# myshare /mnt/shared virtiofs rw,relatime,dax=always 0 0
# DAX 매핑 확인
grep -i dax /proc/$(pidof cat)/smaps
# 7f... rw-s ... /mnt/shared/file (DAX 매핑)
QEMU virtiofsd 주요 옵션
| 옵션 | 설명 | 기본값 | 권장 |
|---|---|---|---|
--cache | 캐싱 정책 | auto | always (읽기 워크로드) |
--sandbox | 격리 모드 | namespace | chroot (성능 우선) |
--thread-pool-size | 워커 스레드 수 | CPU 수 | vCPU 수 이상 |
--inode-file-handles | file handle 사용 | never | mandatory (rename 안정성) |
--announce-submounts | 서브마운트 공유 | off | on (여러 FS 공유 시) |
cache-size (QEMU) | DAX window 크기 | 없음 | 총 파일 크기의 50%+ |
virtiofs 마운트와 공유 메모리 최적화
# 공유 메모리 기반 고성능 설정
# 1. 호스트: hugepages 설정 (2MB 페이지)
echo 4096 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
mkdir -p /dev/hugepages/virtiofs
# 2. QEMU: hugepage 메모리 백엔드 사용
qemu-system-x86_64 \
-object memory-backend-file,id=mem,size=8G,\
mem-path=/dev/hugepages/virtiofs,share=on,\
prealloc=on,host-nodes=0,policy=bind \
-numa node,memdev=mem \
...
# 3. 게스트에서 성능 측정
# 순차 읽기 (DAX)
fio --name=virtiofs_seq --directory=/mnt/shared \
--rw=read --bs=1M --size=4G --direct=1 \
--numjobs=4 --group_reporting
# 예상: ~3,000 MB/s (DAX) vs ~1,200 MB/s (non-DAX)
# 메타데이터 성능
fio --name=virtiofs_meta --directory=/mnt/shared \
--rw=randread --bs=4k --size=1G \
--numjobs=4 --iodepth=32 --group_reporting
# 예상: ~80K IOPS (DAX) vs ~40K IOPS (non-DAX)
# 4. virtiofs 통계 확인
cat /sys/kernel/debug/virtio*/status
mount -t debugfs none /sys/kernel/debug
ls /sys/kernel/debug/virtio-fs/
캐싱 전략
FUSE의 성능은 캐싱 전략에 크게 좌우됩니다. 커널은 페이지 캐시, dentry 캐시, 속성 캐시를 사용하며, FUSE 데몬은 entry_timeout, attr_timeout, writeback_cache 등의 파라미터로 캐시 정책을 제어합니다.
Writeback Cache 설정과 동작
기본 FUSE 쓰기는 writethrough 모드로, 매 write()가 즉시 데몬에 전달됩니다. writeback_cache를 활성화하면 커널이 페이지 캐시에 쓰기를 모아두고, background writeback으로 일괄 전송합니다:
/* writeback cache 활성화 — FUSE_INIT 시 설정 */
static void myfs_init(void *userdata,
struct fuse_conn_info *conn)
{
/* writeback cache 요청 */
if (conn->capable & FUSE_CAP_WRITEBACK_CACHE)
conn->want |= FUSE_CAP_WRITEBACK_CACHE;
/* 관련 설정 */
conn->max_write = 1048576; /* 1MB — 큰 쓰기 배치 */
conn->max_readahead = 1048576; /* 1MB readahead */
conn->max_background = 32; /* 배경 요청 상한 */
conn->congestion_threshold = 24; /* 혼잡 임계값 */
/* auto_cache: 파일 변경 감지 시 자동 무효화 */
if (conn->capable & FUSE_CAP_AUTO_INVAL_DATA)
conn->want |= FUSE_CAP_AUTO_INVAL_DATA;
}
/* High-level API에서 writeback cache 마운트 옵션 */
/* -o writeback_cache — fuse_main() 호출 시 argv에 추가 */
/* writeback 동작 흐름:
* 1. write() → 페이지 캐시에 dirty page 생성
* 2. 즉시 반환 (유저 프로세스 블록 없음)
* 3. 커널 writeback 스레드가 dirty page를 모아서
* 4. FUSE_WRITE 요청으로 데몬에 일괄 전송
* 5. max_write 크기 단위로 병합하여 전송
*/
커널 캐시 정책 (auto_cache, timeout)
/* 캐시 타임아웃 설정 — getattr/lookup 응답 시 */
/* Low-level API: fuse_entry_param으로 제어 */
static void ll_lookup(fuse_req_t req, fuse_ino_t parent,
const char *name)
{
struct fuse_entry_param e;
memset(&e, 0, sizeof(e));
/* ... inode 조회 ... */
/* 캐시 타임아웃 설정 (초 단위) */
e.attr_timeout = 300.0; /* 속성 캐시 5분 */
e.entry_timeout = 300.0; /* dentry 캐시 5분 */
/* 0.0 = 매번 재검증, 큰 값 = 긴 캐시 */
fuse_reply_entry(req, &e);
}
/* High-level API: 마운트 옵션으로 전역 설정 */
/* -o attr_timeout=300 — 속성 캐시 300초 */
/* -o entry_timeout=300 — dentry 캐시 300초 */
/* -o negative_timeout=5 — 부정 엔트리 캐시 5초 */
/* -o auto_cache — mtime 변경 시 자동 무효화 */
/* -o kernel_cache — 파일 데이터를 항상 캐시 */
/* 캐시 정책 비교:
*
* 정책 | 용도 | 일관성
* ───────────────┼───────────────────┼──────────
* timeout=0 | 실시간 원격 FS | 완벽 (느림)
* auto_cache | 단일 클라이언트 | 좋음 (mtime 기반)
* kernel_cache | 읽기 전용 데이터 | 약함 (최고 성능)
* writeback | 쓰기 워크로드 | fsync 시 보장
* timeout=3600 | 정적 아카이브 | 없음 (최고 성능)
*/
auto_cache 모드에서는 open() 시 커널이 파일의 mtime/size를 이전 캐시된 값과 비교합니다. 변경이 감지되면 페이지 캐시를 자동으로 무효화합니다. 단일 클라이언트 환경에서 가장 좋은 일관성/성능 균형을 제공합니다.
벤치마크와 성능 튜닝
FUSE 성능 최적화의 핵심은 병목 지점을 정확히 식별하는 것입니다. 컨텍스트 스위치, 메모리 복사, 직렬화(Serialization) 오버헤드 중 어느 것이 지배적인지에 따라 최적화 전략이 달라집니다.
fio FUSE 벤치마크 프로파일
# 종합 FUSE 벤치마크 스크립트
#!/bin/bash
FUSE_DIR="/mnt/fuse"
NATIVE_DIR="/data/backend"
RESULTS="fuse_bench_results.txt"
echo "=== FUSE Benchmark Suite ===" > "$RESULTS"
echo "Date: $(date)" >> "$RESULTS"
# 1. 순차 읽기 (대역폭 측정)
echo "--- Sequential Read ---" >> "$RESULTS"
fio --name=seq_read \
--directory="$FUSE_DIR" \
--rw=read --bs=1M --size=2G \
--numjobs=4 --group_reporting \
--output-format=json >> "$RESULTS"
# 2. 순차 쓰기 (writeback 효과 측정)
echo "--- Sequential Write ---" >> "$RESULTS"
fio --name=seq_write \
--directory="$FUSE_DIR" \
--rw=write --bs=1M --size=2G \
--numjobs=4 --group_reporting \
--fsync_on_close=1 \
--output-format=json >> "$RESULTS"
# 3. 랜덤 4K 읽기 (IOPS 측정)
echo "--- Random 4K Read ---" >> "$RESULTS"
fio --name=rand_read \
--directory="$FUSE_DIR" \
--rw=randread --bs=4k --size=512M \
--numjobs=8 --iodepth=32 \
--group_reporting \
--output-format=json >> "$RESULTS"
# 4. 혼합 워크로드 (실제 패턴 시뮬레이션)
echo "--- Mixed RW (70/30) ---" >> "$RESULTS"
fio --name=mixed \
--directory="$FUSE_DIR" \
--rw=randrw --rwmixread=70 --bs=8k \
--size=256M --numjobs=4 --iodepth=16 \
--group_reporting \
--output-format=json >> "$RESULTS"
# 5. 메타데이터 집중 (create/stat/unlink)
echo "--- Metadata (create/stat/delete) ---" >> "$RESULTS"
fio --name=metadata \
--directory="$FUSE_DIR"/meta_test \
--rw=randwrite --bs=4k --size=4k \
--nrfiles=10000 --numjobs=1 \
--create_on_open=1 --fallocate=none \
--output-format=json >> "$RESULTS"
max_read/max_write 튜닝
# max_read / max_write 튜닝으로 I/O 요청 크기 최적화
# 현재 설정 확인
cat /sys/fs/fuse/connections/*/max_read
# 131072 (기본 128KB)
cat /sys/fs/fuse/connections/*/max_write
# 131072 (기본 128KB)
# max_write 증가 (마운트 옵션)
./myfs -o max_write=1048576 /mnt/fuse
# 커널이 최대 1MB 단위로 FUSE_WRITE 요청 전송
# max_read는 FUSE_INIT 응답에서 설정
# conn->max_read = 1048576; (C 코드에서)
# max_pages 튜닝 (커널 4.20+)
# 단일 요청에 포함 가능한 최대 페이지 수
mount -t fuse -o max_pages=256 myfs /mnt/fuse
# 256 * 4KB = 1MB per request
# 튜닝 효과 측정
# max_write=128K: 순차 쓰기 ~600 MB/s
# max_write=1M: 순차 쓰기 ~1,200 MB/s (2배 향상)
# max_write=4M: 순차 쓰기 ~1,500 MB/s (수확 체감)
# max_background / congestion_threshold
# 백그라운드 요청(readahead, writeback) 최대 수
mount -o max_background=64,congestion_threshold=48 ...
# 높은 동시성 워크로드에서 파이프라인 깊이 증가
splice/zero-copy 성능 비교
# splice (write_buf) 활성화/비활성화 성능 비교
# write_buf 미구현 (일반 write 콜백만 있는 경우)
# 커널 → 유저 버퍼 복사 → 데몬 처리 → backing FS
./myfs_no_splice -f /mnt/fuse_nosplice
fio --name=nosplice --directory=/mnt/fuse_nosplice \
--rw=write --bs=1M --size=2G --numjobs=1
# 결과: BW=680 MB/s, CPU user=12%, sys=35%
# write_buf 구현 (splice zero-copy)
# 커널 → pipe → backing fd (유저 버퍼 복사 없음)
./myfs_splice -f /mnt/fuse_splice
fio --name=splice --directory=/mnt/fuse_splice \
--rw=write --bs=1M --size=2G --numjobs=1
# 결과: BW=1,350 MB/s, CPU user=3%, sys=22%
# passthrough (6.9+)
# 커널이 backing fd로 직접 I/O (데몬 완전 우회)
./myfs_passthrough -f /mnt/fuse_pt
fio --name=passthrough --directory=/mnt/fuse_pt \
--rw=write --bs=1M --size=2G --numjobs=1
# 결과: BW=3,200 MB/s, CPU user=0.1%, sys=15%
# perf로 CPU 프로파일링 (병목 확인)
perf record -g -p $(pidof myfs_no_splice) -- sleep 10
perf report --sort=comm,dso,symbol
# copy_user_generic_unrolled → 메모리 복사 병목 확인
# 요약: 동일 백엔드에서의 순차 쓰기 성능
# ┌──────────────────┬──────────┬───────────┐
# │ 모드 │ 대역폭 │ CPU 사용률│
# ├──────────────────┼──────────┼───────────┤
# │ 일반 write │ 680MB/s │ 47% │
# │ splice/write_buf │ 1350MB/s │ 25% │
# │ passthrough │ 3200MB/s │ 15% │
# │ native ext4 │ 3500MB/s │ 12% │
# └──────────────────┴──────────┴───────────┘
참고 자료
- docs.kernel.org — FUSE — 커널 공식 FUSE 파일시스템 문서입니다.
- docs.kernel.org — virtiofs — virtio 기반 호스트-게스트 파일 공유를 위한 FUSE 변형 문서입니다.
- fs/fuse/ — Bootlin Elixir — FUSE 커널 모듈 전체 소스 디렉터리입니다.
- fs/fuse/dev.c — Bootlin Elixir — /dev/fuse 디바이스 통신 계층 구현 소스 코드입니다.
- fs/fuse/inode.c — Bootlin Elixir — FUSE 슈퍼블록 및 아이노드 관리 구현 소스 코드입니다.
- fs/fuse/file.c — Bootlin Elixir — FUSE 파일 연산(read/write/mmap) 구현 소스 코드입니다.
- fs/fuse/dir.c — Bootlin Elixir — FUSE 디렉터리 연산(lookup/readdir) 구현 소스 코드입니다.
- fs/fuse/passthrough.c — Bootlin Elixir — FUSE passthrough I/O 최적화 구현 소스 코드입니다.
- include/uapi/linux/fuse.h — Bootlin Elixir — FUSE 커널-유저 프로토콜 헤더 정의입니다.
- GitHub — libfuse/libfuse — FUSE 사용자 공간 라이브러리(libfuse) 공식 저장소입니다.
- mount.fuse(8) — libfuse — FUSE 마운트 도구 매뉴얼 페이지입니다.
- GitHub — libfuse/sshfs — SSH 프로토콜 기반 FUSE 파일시스템 대표 구현체입니다.
- LWN: FUSE — Filesystem in Userspace — FUSE 프로젝트 초기 소개 및 설계 철학을 다룬 기사입니다.
- LWN: A new approach to FUSE writeback — FUSE writeback 캐시 모드의 도입 배경과 구현을 설명하는 기사입니다.
- LWN: FUSE passthrough mode — FUSE passthrough I/O 최적화의 설계와 성능 효과를 분석한 기사입니다.
- LWN: Sharing files with virtiofs — virtiofs의 아키텍처와 DAX 창을 활용한 성능 최적화를 소개하는 기사입니다.
- man fuse(4) — FUSE 디바이스 파일(/dev/fuse) 매뉴얼 페이지입니다.
- man mount.fuse3(8) — FUSE3 마운트 헬퍼 매뉴얼 페이지입니다.
관련 문서
FUSE와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.