FUSE (Filesystem in Userspace)
FUSE는 파일시스템 로직을 커널 밖 사용자 공간에서 구현하게 해주는 프레임워크입니다. 이 문서에서는 `/dev/fuse` 요청/응답 프로토콜, fuse.ko와 libfuse의 역할 분리, lookup/read/write/flush 연산 경로, page cache와 writeback 캐시 정책, 권한 위임과 uid/gid 매핑, virtiofs 기반 가상화 시나리오, 컨텍스트 스위치 오버헤드와 병목 최적화 기법을 상세히 다룹니다.
핵심 요약
- 계층 이해 — VFS, 캐시, 하위 FS 경계를 구분합니다.
- 메타데이터 우선 — inode/dentry 일관성을 먼저 확인합니다.
- 저장 정책 — 저널링/압축/할당 정책 차이를 비교합니다.
- 일관성 모델 — 로컬/원격/합성 FS의 반영 시점을 구분합니다.
- 복구 관점 — 장애 시 재구성 경로를 함께 점검합니다.
단계별 이해
- 경계 계층 파악
요청이 VFS에서 어디로 내려가는지 확인합니다. - 메타/데이터 분리
어느 경로에서 무엇이 갱신되는지 나눠 봅니다. - 동기화/플러시 확인
쓰기 반영 시점과 순서를 검증합니다. - 복구 시나리오 점검
비정상 종료 후 일관성 회복을 확인합니다.
FUSE 개요
FUSE(Filesystem in Userspace)는 커널 모듈을 직접 작성하지 않고도 유저스페이스 프로그램으로 파일시스템을 구현할 수 있게 해주는 프레임워크입니다. 2005년 Linux 2.6.14에서 mainline에 통합되었으며, 현재까지 수백 개의 FUSE 기반 파일시스템이 활발히 사용되고 있습니다.
유저스페이스 파일시스템이 필요한 이유
- 개발 용이성 — 커널 프로그래밍 없이 C, Python, Go, Rust 등 일반 언어로 파일시스템 구현 가능
- 안정성 — 유저스페이스 프로세스 크래시가 커널 패닉을 유발하지 않음
- 빠른 프로토타이핑 — 커널 재컴파일 없이 파일시스템 로직을 즉시 수정/테스트
- 라이브러리 활용 — OpenSSL, libcurl, gRPC 등 유저스페이스 라이브러리를 자유롭게 사용
- non-root 마운트 — 일반 사용자도 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()등 시스템 콜 호출 - VFS가 FUSE 파일시스템의
file_operations/inode_operations을 통해 FUSE 커널 모듈로 디스패치 - FUSE 커널 모듈이
fuse_req구조체를 생성하여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 참조 카운트 감소 (응답 없음) |
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: 커널 → 유저 비동기 알림
커널은 유저 데몬에 비동기 알림을 보낼 수 있습니다. 이는 캐시 무효화 등에 사용됩니다:
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과 이벤트 루프
/* 싱글스레드 이벤트 루프 */
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 파일시스템 예제입니다. 읽기 전용으로 /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
성능 특성과 최적화
유저-커널 컨텍스트 스위칭 오버헤드
FUSE의 근본적인 성능 제약은 모든 파일시스템 요청이 커널 ↔ 유저스페이스 간 컨텍스트 스위칭을 수반한다는 점입니다. 단일 read() 호출이 최소 4번의 컨텍스트 스위칭을 발생시킵니다:
- 애플리케이션 → 커널 (syscall)
- 커널 → FUSE 데몬 (/dev/fuse read)
- FUSE 데몬 → 커널 (/dev/fuse write)
- 커널 → 애플리케이션 (syscall return)
FUSE writeback cache (kernel 3.15+)
writeback cache를 활성화하면 커널이 쓰기 데이터를 페이지 캐시에 버퍼링하여 배치 처리합니다. 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
컨테이너 환경 활용
Kata Containers는 virtiofs를 사용하여 컨테이너 이미지와 볼륨을 경량 VM 내부에 공유합니다. 이를 통해 컨테이너 수준의 편의성과 VM 수준의 격리를 동시에 달성합니다.
보안 모델
non-root 마운트와 보안 고려사항
FUSE는 일반 사용자의 파일시스템 마운트 시 가장 널리 사용되는 커널 메커니즘 중 하나입니다. 이로 인해 다음과 같은 보안 고려사항이 존재합니다:
- 기본 접근 제한 — FUSE 마운트는 기본적으로 마운트한 사용자만 접근 가능 (다른 사용자/root도 접근 불가)
- 데몬 신뢰성 — FUSE 데몬이 악의적인 응답을 반환할 수 있음 (예: getattr에서 거짓 권한 반환)
- DoS 가능성 — 데몬이 응답하지 않으면 해당 마운트에 접근하는 프로세스가 영구 대기(hang)
default_permissions vs 커스텀 접근 제어
| 모드 | 권한 검사 위치 | 특징 |
|---|---|---|
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 | 암호화 파일시스템 | 파일 단위 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 | 사용 | 배포/디버깅이 유연, 커널 업데이트 의존도 낮음 |
| CephFS 커널 클라이언트 | VFS → fs/ceph → Ceph | 미사용 | 커널 경로 직결, 컨텍스트 스위칭 오버헤드 감소 |
| RBD | 블록 계층 → rbd 모듈 → Ceph | 미사용 | 파일시스템이 아닌 블록 디바이스 경로 |
문맥 구분: FUSE 성능/디버깅 이슈를 분석할 때는 ceph-fuse 경로만 같은 범주로 비교해야 합니다. 커널 클라이언트나 RBD 수치는 FUSE 오버헤드 분석에 직접 대입하면 왜곡됩니다.
ceph-fuse 운영 체크포인트
- 지연 급증 —
/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 메시지는 고정 크기 헤더 + 가변 크기 페이로드로 구성됩니다. 요청(커널→데몬)은 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 원자적 |
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()를 호출합니다. 멀티스레드 처리 시 여러 스레드가 동일 fd에서 경쟁적으로 읽기를 수행합니다. Linux 4.2+에서 도입된 clone_fd 옵션은 각 스레드에 독립적인 fd를 제공하여 락 경합을 줄입니다.
/* 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: 요청 취소 메커니즘
애플리케이션이 시그널로 인해 시스템 콜을 중단하면, 커널은 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 콜백과 일반 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) | 빠름 (캐시 히트) |
| 크래시 시 데이터 | 안전 (이미 전달) | 유실 가능 (더티 페이지) |
| 다중 접근 일관성 | 강한 일관성 | 약한 일관성 (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의 주소 공간에 직접 매핑하는 기술입니다. 데이터 복사 없이 메모리 매핑된 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 요청 파이프라인에서 추적 가능한 지점을 보여줍니다.
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 | 대용량 순차 읽기 처리량 향상 | 데몬 메모리 사용량 증가 |
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와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.