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) 최적화 기법을 상세히 다룹니다.

전제 조건: VFS네트워크 스택(Network Stack) 문서를 먼저 읽으세요. 원격/합성 파일시스템은 로컬 경로와 다른 지연(Latency)·일관성 모델을 가지므로, 경계 계층(커널/유저/원격)을 먼저 구분해야 합니다.
일상 비유: 이 주제는 여러 창고를 겹쳐 보이는 통합 창고와 비슷합니다. 실제 보관 위치와 사용자에게 보이는 경로가 다를 수 있어, 조회 경로와 갱신 반영 시점을 분리해서 보는 것이 핵심입니다.

핵심 요약

  • 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 기반 기술입니다.

단계별 이해

  1. FUSE 3계층 구조 파악 — 커널 모듈, /dev/fuse, 유저스페이스 데몬의 역할을 구분합니다.

    VFS → FUSE 커널 모듈 → /dev/fuse → 유저스페이스 데몬 경로에서 각 계층의 책임 범위를 이해합니다.

  2. 요청-응답 프로토콜 이해 — 커널과 유저스페이스 사이의 메시지 교환 과정을 추적합니다.

    fuse_req가 큐에 들어가고, 데몬이 read()로 가져가 처리한 뒤 write()로 응답하는 흐름을 따라갑니다.

  3. libfuse로 간단한 FS 구현 — high-level API를 사용하여 최소한의 FUSE 파일시스템을 직접 작성해봅니다.

    getattr, readdir, open, read 콜백만 구현하면 기본적인 읽기 전용 파일시스템을 만들 수 있습니다.

  4. 성능 특성 분석 — 유저스페이스 경유로 인한 오버헤드와 최적화 방안을 비교합니다.

    컨텍스트 스위치(Context Switch) 비용, splice/passthrough 모드, writeback 캐시 등 성능 개선 기법을 검토합니다.

  5. 실제 활용 사례 탐구 — SSHFS, GlusterFS, virtiofs 등 실전 FUSE 파일시스템의 구조를 살펴봅니다.

    각 사례에서 FUSE의 장점(개발 용이성, 안정성)과 한계(성능 오버헤드)가 어떻게 균형을 이루는지 확인합니다.

관련 표준: FUSE Protocol (커널 ↔ 유저스페이스 메시지 프로토콜), POSIX.1-2017 (파일 시맨틱) — FUSE는 사용자 공간에서 POSIX 호환 파일시스템을 구현할 수 있게 합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

FUSE 개요

FUSE(Filesystem in Userspace)는 커널 모듈(Kernel Module)을 직접 작성하지 않고도 유저스페이스 프로그램으로 파일시스템을 구현할 수 있게 해주는 프레임워크입니다. 2005년 Linux 2.6.14에서 mainline에 통합되었으며, 현재까지 수백 개의 FUSE 기반 파일시스템이 활발히 사용되고 있습니다.

유저스페이스 파일시스템이 필요한 이유

FUSE 3계층 구조

FUSE는 세 가지 핵심 컴포넌트로 구성됩니다:

계층컴포넌트역할
커널 fuse.ko 모듈 VFS 요청을 /dev/fuse 디바이스를 통해 유저스페이스로 전달
라이브러리 libfuse (libfuse3) /dev/fuse와의 통신을 추상화, high-level/low-level API 제공
유저스페이스 FUSE 데몬 실제 파일시스템 로직 구현 (sshfs, ntfs-3g 등)

FUSE 아키텍처

FUSE 요청/응답 흐름 User Space Application: open(), read() FUSE Daemon (sshfs 등) libfuse3 (fuse_operations) Kernel Space System Call Interface VFS Layer FUSE Kernel Module (fuse.ko) fuse_conn / fuse_req / /dev/fuse /dev/fuse (char device) read() write() 1. syscall 2. VFS dispatch 3. queue req 4. callback 응답: 역순으로 결과 반환
FUSE 요청/응답 흐름: 애플리케이션 syscall → VFS → FUSE 커널 모듈 → /dev/fuse → libfuse → 유저 데몬

요청/응답 흐름 상세

  1. 애플리케이션이 open(), read() 등 시스템 콜(System Call) 호출
  2. VFS가 FUSE 파일시스템의 file_operations/inode_operations을 통해 FUSE 커널 모듈로 디스패치(Dispatch)
  3. FUSE 커널 모듈이 fuse_req 구조체(Struct)를 생성하여 fuse_iqueue에 큐잉
  4. 유저 데몬이 /dev/fuse에서 read()로 요청을 가져옴
  5. 유저 데몬이 요청을 처리 (파일 읽기, 네트워크 I/O 등)
  6. 유저 데몬이 /dev/fusewrite()로 응답을 기록
  7. 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는 두 가지 큐를 사용하여 요청을 관리합니다:

요청 라이프사이클

/* 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_LOOKUP1경로명으로 inode 탐색
FUSE_FORGET2inode 참조 카운트(Reference Count) 감소 (응답 없음)
FUSE_GETATTR3파일 속성 조회 (stat)
FUSE_SETATTR4파일 속성 변경 (chmod, chown, truncate)
FUSE_OPEN14파일 열기
FUSE_READ15파일 읽기
FUSE_WRITE16파일 쓰기
FUSE_RELEASE18파일 닫기
FUSE_OPENDIR27디렉토리 열기
FUSE_READDIR28디렉토리 목록 읽기
FUSE_RELEASEDIR29디렉토리 닫기
FUSE_MKDIR9디렉토리 생성
FUSE_UNLINK10파일 삭제
FUSE_RMDIR11디렉토리 삭제
FUSE_RENAME12파일/디렉토리 이름 변경
FUSE_LINK13하드 링크 생성
FUSE_SYMLINK6심볼릭 링크 생성
FUSE_CREATE35파일 생성 + 열기 (atomic)
FUSE_STATFS17파일시스템 통계
FUSE_INIT26연결 초기화 (핸드셰이크)
FUSE_DESTROY38연결 종료
FUSE_NOTIFY_REPLY41비동기 알림 응답
FUSE_READDIRPLUS44readdir + 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) 등에 사용됩니다:

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_rootroot만 추가 접근 허용비활성
default_permissions커널이 권한 검사 수행 (데몬에 위임하지 않음)비활성
max_read=N단일 read 요청 최대 크기 (바이트)131072
max_write=N단일 write 요청 최대 크기 (바이트)131072
max_background=N최대 동시 배경 요청 수12
congestion_threshold=NBDI 혼잡 시작 임계치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번의 컨텍스트 스위칭을 발생시킵니다:

  1. 애플리케이션 → 커널 (syscall)
  2. 커널 → FUSE 데몬 (/dev/fuse read)
  3. FUSE 데몬 → 커널 (/dev/fuse write)
  4. 커널 → 애플리케이션 (syscall return)

FUSE writeback cache (kernel 3.15+)

writeback cache를 활성화하면 커널이 쓰기 데이터를 페이지 캐시(Page Cache)에 버퍼(Buffer)링하여 배치 처리합니다. FUSE_INITFUSE_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 ext4FUSE (기본)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 큐를 통해 전달합니다.

virtiofs 아키텍처 Guest VM Application VFS → FUSE Kernel Module virtio-fs driver (virtqueue) DAX window (선택적) Host QEMU / vhost-user-fs virtiofsd (FUSE daemon) Host Filesystem (ext4, XFS ...) virtio
virtiofs: 게스트 VM의 FUSE 요청을 virtio 전송으로 호스트 virtiofsd에 전달

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는 일반 사용자의 파일시스템 마운트 시 가장 널리 사용되는 커널 메커니즘 중 하나입니다. 이로 인해 다음과 같은 보안 고려사항이 존재합니다:

default_permissions vs 커스텀 접근 제어(Access Control)

모드권한 검사 위치특징
default_permissions 커널 (VFS) 표준 Unix 권한 모델 (uid/gid/mode). 안전하고 예측 가능
커스텀 (기본) FUSE 데몬 데몬이 자체 ACL/인증 로직 구현 가능. 유연하지만 데몬 버그 시 보안 취약

allow_other / allow_root

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-fuseVFS → FUSE → 유저 데몬 → Ceph사용배포/디버깅(Debugging)이 유연, 커널 업데이트 의존도 낮음
CephFS 커널 클라이언트VFS → fs/ceph → Ceph미사용커널 경로 직결, 컨텍스트 스위칭 오버헤드 감소
RBD블록 계층 → rbd 모듈 → Ceph미사용파일시스템이 아닌 블록 디바이스 경로
⚠️

문맥 구분: FUSE 성능/디버깅 이슈를 분석할 때는 ceph-fuse 경로만 같은 범주로 비교해야 합니다. 커널 클라이언트나 RBD 수치는 FUSE 오버헤드 분석에 직접 대입하면 왜곡됩니다.

ceph-fuse 운영 체크포인트(Checkpoint)

💡

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 필드가 요청-응답 매칭의 핵심입니다.

/dev/fuse 프로토콜 메시지 구조 요청 메시지 (커널 → 데몬) fuse_in_header (40 bytes) len (4B) opcode (4B) unique (8B) nodeid (8B) uid/gid/pid (12B) pad (4B) opcode별 페이로드 (가변 크기) 응답 메시지 (데몬 → 커널) fuse_out_header (16 bytes) len (4B) error (4B) unique (8B) opcode별 응답 데이터 (가변) unique 매칭 프로토콜 시퀀스 커널 (fuse.ko) FUSE 데몬 FUSE_INIT (opcode=26) FUSE_INIT 응답 (버전/플래그 협상) FUSE_LOOKUP (nodeid=1, name="file.txt") entry_out (nodeid, attr, timeout) FUSE_OPEN (nodeid=2, flags=O_RDONLY) open_out (fh, open_flags) FUSE_READ (fh, offset, size) 데이터 바이트 (len - sizeof(out_header))
/dev/fuse 프로토콜: 요청/응답 메시지 형식과 INIT→LOOKUP→OPEN→READ 시퀀스

opcode별 요청/응답 페이로드

Opcode요청 구조체응답 구조체비고
FUSE_LOOKUP이름 문자열 (null-terminated)fuse_entry_out응답에 attr timeout 포함
FUSE_FORGETfuse_forget_in없음 (no reply)nlookup 감소
FUSE_GETATTRfuse_getattr_infuse_attr_outattr_valid 시간 포함
FUSE_OPENfuse_open_infuse_open_outfh (파일 핸들) 반환
FUSE_READfuse_read_inraw 데이터 바이트size만큼 데이터 반환
FUSE_WRITEfuse_write_in + 데이터fuse_write_out실제 기록 바이트 수
FUSE_CREATEfuse_create_in + 이름fuse_entry_out + fuse_open_outlookup+open 원자적(Atomic)
FUSE_SETATTRfuse_setattr_infuse_attr_outvalid 비트마스크로 변경 필드 지정
FUSE_READDIRfuse_read_infuse_dirent 배열각 엔트리 8바이트 정렬
FUSE_READDIRPLUSfuse_read_infuse_direntplus 배열readdir + 각 엔트리 attr 포함
FUSE_INITfuse_init_infuse_init_out프로토콜 버전/기능 협상
FUSE_BATCH_FORGETfuse_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 I/O 경로: 일반 vs splice Zero-Copy 일반 경로 (2회 복사) 커널 페이지 캐시 복사1 유저스페이스 데몬 버퍼 /dev/fuse 버퍼 복사2 splice 경로 (Zero-Copy) 커널 페이지 캐시 커널 파이프 (페이지 참조만) /dev/fuse 버퍼 참조 splice splice 읽기/쓰기 데이터 흐름 앱 read() VFS/FUSE /dev/fuse pipe (splice) 데몬 splice_read splice_read: 데몬이 pipe에서 직접 헤더+데이터를 읽음 (복사 없음) 데몬 splice_write pipe (splice) /dev/fuse FUSE 모듈 페이지 캐시 splice_write: 데몬이 pipe를 통해 응답 데이터를 직접 전달 (복사 없음) splice 활성화: FUSE_INIT에서 FUSE_SPLICE_READ | FUSE_SPLICE_WRITE 플래그 대용량 I/O에서 최대 2배 처리량 향상 (128KB+ 블록)
FUSE splice zero-copy: 일반 경로(2회 복사) vs splice 경로(페이지(Page) 참조만 이동)

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);
}
성능 팁: splice zero-copy는 128KB 이상의 대용량 순차 I/O에서 가장 효과적입니다. 4KB 소규모 랜덤 I/O에서는 splice 셋업 오버헤드로 인해 오히려 성능이 저하될 수 있으므로, write_buf 콜백(Callback)과 일반 write 콜백을 워크로드에 맞게 선택하세요.

Passthrough 모드 (커널 6.9+)

FUSE passthrough는 데이터 I/O(read/write)를 유저스페이스 데몬을 거치지 않고 커널에서 직접 백엔드(backing) 파일에 수행하는 기능입니다. 메타데이터 연산(lookup, getattr, open 등)만 데몬이 처리하고, 실제 데이터 경로는 커널이 네이티브 파일시스템처럼 직접 처리합니다. 이로써 FUSE의 가장 큰 병목인 컨텍스트 스위칭과 데이터 복사를 완전히 제거할 수 있습니다.

FUSE Passthrough: 메타데이터 vs 데이터 경로 분리 Application (open, read, write, stat) VFS Layer FUSE Kernel Module (fuse.ko) passthrough 라우팅 결정 메타데이터 경로 /dev/fuse (요청 큐) FUSE Daemon lookup, getattr, open, setattr, mkdir, unlink ... 데이터 경로 (Passthrough) Backing File (커널 직접) 하위 FS (ext4, XFS ...) read, write, mmap (데몬 바이패스, 네이티브 성능) 데이터는 데몬 안 거침 설정: FUSE_INIT에서 FUSE_PASSTHROUGH 협상 OPEN 응답 시 passthrough_fh = backing_fd 전달 → 이후 READ/WRITE는 커널이 직접 처리
FUSE passthrough: 메타데이터는 데몬이, 데이터 I/O는 커널이 backing file에 직접 수행

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 워크로드에서 네이티브 파일시스템에 근접한 성능을 달성할 수 있습니다.

제한 사항: passthrough 모드는 커널 6.9 이상에서만 사용 가능하며, libfuse 3.16+가 필요합니다. 또한 backing file의 파일시스템이 반드시 로컬 네이티브 FS(ext4, XFS 등)여야 합니다. 네트워크 파일시스템이나 다른 FUSE 파일시스템을 backing으로 사용할 수 없습니다.

Writeback 캐시 모델

FUSE의 기본 쓰기 모드는 write-through로, 모든 쓰기가 즉시 데몬에 전달됩니다. writeback 캐시를 활성화하면 커널 페이지 캐시가 쓰기를 버퍼링하고, 적절한 시점에 배치로 데몬에 플러시합니다. 이는 소규모 쓰기가 빈번한 워크로드에서 극적인 성능 향상을 가져옵니다.

FUSE Writeback Cache 동작 모델 Write-Through (기본 모드) 앱 write() VFS FUSE 모듈 (즉시 전달) /dev/fuse 데몬 (매번 처리) Writeback Cache 모드 앱 write() VFS 페이지 캐시 더티 페이지 버퍼링 앱에 즉시 반환! 플러시 트리거 /dev/fuse 데몬 (배치) Writeback 플러시 트리거 1. dirty_expire_centisecs 더티 페이지 만료 시간 (기본 30초) 2. dirty_ratio / dirty_bytes 더티 페이지 비율 임계치 도달 시 동기 플러시 3. fsync() / fdatasync() 명시적 동기화 요청 4. close() / FUSE_RELEASE 파일 닫기 시 모든 더티 페이지 플러시 5. 메모리 압박 시스템 메모리 부족 시 페이지 회수를 위해 플러시
FUSE writeback 캐시: write-through(즉시 전달) vs writeback(페이지 캐시 버퍼링 후 배치 플러시)

일관성 모델과 주의사항

writeback 캐시는 성능과 일관성 사이의 트레이드오프입니다:

항목Write-ThroughWriteback 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);
}
주의: writeback 캐시 모드에서는 같은 파일을 여러 노드에서 동시에 수정하면 데이터 충돌이 발생할 수 있습니다. 분산 파일시스템(GlusterFS, CephFS 등)에서 writeback을 사용할 때는 반드시 분산 잠금(Lock) 메커니즘을 함께 구현해야 합니다.

DAX (Direct Access) 모드

FUSE DAX는 호스트 메모리의 파일 데이터를 게스트 VM의 주소 공간(Address Space)에 직접 매핑하는 기술입니다. 데이터 복사 없이 메모리 매핑된 I/O(mmap)가 가능하여, virtiofs 환경에서 네이티브에 가까운 성능을 제공합니다. 이 모드는 주로 virtiofs + QEMU 조합에서 사용됩니다.

FUSE DAX: 호스트 페이지 캐시 직접 매핑 Guest VM Application (mmap / read / write) VFS → FUSE Module DAX Window (공유 메모리 영역) VIRTIO_FS_SHMCAP_ID_CACHE 매핑 영역 A file1.txt 매핑 영역 B file2.dat virtio-fs driver (virtqueue) FUSE_SETUPMAPPING / FUSE_REMOVEMAPPING (메타데이터만 virtqueue 사용) Host QEMU (vhost-user-fs-pci) virtiofsd (FUSE daemon) --cache=always 호스트 페이지 캐시 공유 메모리 (memfd) file1.txt 페이지 호스트 캐시 file2.dat 페이지 호스트 캐시 Host FS (ext4, XFS ...) 직접 메모리 매핑 (복사 없음) DAX 장점: mmap() 시 page fault로 호스트 캐시를 직접 접근 (zero-copy)
FUSE DAX: 게스트 VM의 DAX window가 호스트 페이지 캐시를 직접 참조하여 데이터 복사 제거

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-sizeDAX window 크기 (QEMU)VM 메모리의 25~50%
--cache=alwaysvirtiofsd 캐시 정책DAX 사용 시 필수
dax=always게스트 마운트 옵션항상 DAX 사용
dax=inode게스트 마운트 옵션파일별 DAX 선택 (6.2+)
dax=never게스트 마운트 옵션DAX 비활성화
DAX window 관리: DAX window는 고정 크기이므로, 매핑된 파일 영역이 window를 초과하면 LRU 기반으로 기존 매핑이 해제됩니다. 매우 큰 파일 세트를 동시에 접근하는 워크로드에서는 window 크기를 충분히 확보해야 매핑 교체(thrashing)를 방지할 수 있습니다.

virtiofs 아키텍처

virtiofs는 FUSE 프로토콜을 virtio 전송 계층 위에서 사용하여 호스트-게스트 간 고성능 파일시스템 공유를 구현합니다. 기존 9p(virtio-9p)보다 성능이 우수하며, FUSE의 풍부한 캐시 정책을 그대로 활용할 수 있습니다.

virtiofs 상세 아키텍처: QEMU/KVM 통합 Guest VM (Linux Kernel) Application (POSIX API) VFS Layer FUSE Kernel Module (virtio-fs) fs/fuse/virtio_fs.c hiprio VQ request VQ(s) FORGET 등 응답 불필요 요청 일반 FUSE 요청 (멀티 큐 지원) DAX Window (Shared Memory Region) PCI BAR 기반 공유 메모리 notification VQ (캐시 무효화 수신) 게스트 페이지 캐시 게스트 inode/dentry 캐시 캐시 정책: none / auto / always vhost-user 소켓 Host QEMU (vhost-user-fs-pci) chardev socket 연결 virtiofsd (Rust) vhost-user backend / FUSE 요청 처리 FUSE 요청 파싱 ID 매핑 (sandboxing) Host Syscall (openat2, fstatat, ...) 호스트 페이지 캐시 (공유 메모리) memory-backend-memfd, share=on 호스트 VFS / 로컬 FS (ext4, XFS ...) 호스트 블록 디바이스 (NVMe, SSD) 보안: --sandbox chroot|namespace 공유 디렉토리 외부 접근 차단
virtiofs 상세 아키텍처: 게스트 FUSE 모듈 → virtqueue → vhost-user 소켓(Socket) → virtiofsd → 호스트 FS

virtiofs vs 9p(virtio-9p) 비교

항목virtiofs9p (virtio-9p)
프로토콜FUSE9P2000.L
캐시 정책none / auto / always / DAXnone / 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 커널 내부: 핵심 구조체 관계도 fuse_mount super_block 연결, /dev/fuse fd fuse_conn connected, max_read/write, max_pages writeback_cache, no_open, parallel_dirops max_background, congestion_threshold fuse_iqueue pending 리스트 (대기 요청) waitq, connected, ops iq fuse_pqueue processing 해시 테이블 io 리스트 (진행 중 I/O) pq (per /dev/fuse fd) fuse_req unique (요청 ID), in/out 헤더 waitq, args, background, force 상태: PENDING → SENT → REPLIED 대기 큐 처리 큐 fuse_args in_args[], out_args[] fuse_inode nodeid, nlookup, attr_valid 소스 파일 구성 (fs/fuse/) inode.c | dir.c | file.c | dev.c | control.c | virtio_fs.c | passthrough.c | dax.c
FUSE 커널 내부: fuse_conn을 중심으로 fuse_iqueue(입력), fuse_pqueue(처리), fuse_req(요청) 관계

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 기반 파일시스템의 구현 특성을 비교하여, 프로젝트 요구사항에 맞는 구현체를 선택할 수 있도록 합니다. 아래 다이어그램은 주요 구현체의 아키텍처 분류를 보여줍니다.

FUSE 구현체 분류: 백엔드 유형별 FUSE / libfuse3 커널 ↔ 유저스페이스 프레임워크 로컬/블록 백엔드 NTFS-3G (NTFS 파티션) gocryptfs (암호화 래퍼) mergerfs (디스크 풀링) SquashFUSE (읽기 전용) 네트워크/원격 SSHFS (SFTP 프로토콜) CurlFtpFS (FTP/FTPS) NFS-FUSE (NFS 유저스페이스) 클라우드/오브젝트 s3fs-fuse (Amazon S3) rclone mount (40+ 클라우드) gcsfuse (Google Cloud) 분산 파일시스템 GlusterFS (FUSE 클라이언트) ceph-fuse (CephFS) JuiceFS (메타데이터 엔진) 가상화/컨테이너 virtiofsd (호스트-게스트) FUSE-OverlayFS (rootless) Kata Containers FUSE
FUSE 구현체 분류: 로컬/블록, 네트워크, 클라우드, 분산, 가상화 백엔드별 대표 프로젝트
구현체언어API 레벨멀티스레드splicewritebackpassthrough주요 용도
SSHFSCHigh-level지원지원지원미지원SSH 원격 마운트
GlusterFSCLow-level지원지원부분미지원분산 스토리지
s3fs-fuseC++High-level지원미지원지원미지원S3 객체 스토리지
NTFS-3GCLow-level미지원미지원미지원미지원NTFS 읽기/쓰기
ceph-fuseC++Low-level지원미지원지원미지원분산 FS 클라이언트
rclone mountGocgofuse지원미지원지원미지원클라우드 스토리지
gocryptfsGogo-fuse지원미지원미지원미지원파일 암호화
mergerfsC++Low-level지원지원미지원지원(6.9+)디스크 풀링
CurlFtpFSCHigh-level미지원미지원미지원미지원FTP 마운트
virtiofsdRust전용지원N/A지원지원VM 파일 공유

언어별 FUSE 바인딩

언어라이브러리API 유형특징
Clibfuse3High/Low-level공식 레퍼런스 구현, 가장 완전한 기능
Rustfuse3 / fuserLow-level메모리 안전성, async/await 지원
Gogo-fuse / cgofuseLow-levelgoroutine 기반 병렬 처리
Pythonpyfuse3 / fusepyHigh-level프로토타이핑에 적합, 성능 제약
Javajnr-fuseHigh-levelJNI 기반, JVM 생태계 활용
C++libfuse3 (직접)High/Low-levelC 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)에서 추적 가능한 지점을 보여줍니다.

FUSE 추적 지점 (Tracepoint / kprobe) 앱 syscall tracepoint: FUSE 큐잉 kprobe: ctx switch sched: /dev/fuse I/O kprobe: 데몬 처리 uprobe: sys_enter_read sys_enter_write sys_enter_open sys_exit_* fuse_simple_request fuse_simple_background fuse_request_send fuse_queue_iqueue sched_switch sched_wakeup (지연시간 핵심) 컨텍스트 스위칭 비용 fuse_dev_read fuse_dev_write fuse_dev_splice_read fuse_dev_splice_write fuse_ops.read fuse_ops.write fuse_ops.lookup (데몬 함수) 측정 가능한 지연시간 구간 T1: 전체 요청 지연 (fuse_simple_request 진입 ~ 반환) T2: 커널 측 지연 (큐잉 + 스위칭 + wake) T3: 데몬 처리 시간 (read → 처리 → write) T4: 컨텍스트 스위칭 (sched_switch 기반) T1 = T2 + T3 = (큐잉 + T4 x 2) + (데몬 처리 + 백엔드 I/O)
FUSE 추적 지점: 각 파이프라인 단계에서 ftrace/bpftrace로 측정 가능한 함수와 지연시간 구간

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 파일시스템의 성능은 아키텍처 특성상 네이티브 파일시스템보다 제한적이지만, 적절한 튜닝으로 상당한 성능 향상을 달성할 수 있습니다. 이 섹션에서는 병목 분석 방법론과 구체적인 튜닝 파라미터를 다룹니다.

FUSE 성능 병목 지점과 최적화 전략 요청 경로별 지연시간 분해 Syscall 진입 ~0.5us 최소 VFS + 큐잉 ~2-5us 락 경합 가능 컨텍스트 스위칭 ~5-15us x2 최대 병목! 데몬 처리 가변 (1us~ms) 구현 의존 데이터 복사 크기 비례 splice로 제거 병목별 최적화 전략 컨텍스트 스위칭 제거 1. passthrough 모드 (6.9+) 2. DAX 모드 (virtiofs) 3. 커널 캐시 활용 (attr/entry timeout) 4. READDIRPLUS (lookup 횟수 감소) 5. no_open (OPEN 요청 생략) 효과: 데이터 I/O 최대 10배 향상 데이터 복사 제거 1. splice read/write 2. FUSE_BUF_SPLICE_MOVE 3. write_buf 콜백 (read 대신) 4. max_pages 증가 (배치 크기) 5. max_read/max_write 증가 효과: 대용량 I/O 최대 2배 병렬성 향상 1. 멀티스레드 (clone_fd) 2. max_background 증가 3. parallel_dirops 활성화 4. writeback 캐시 (배치 쓰기) 5. congestion_threshold 조정 효과: 멀티코어 확장성 워크로드별 권장 설정 워크로드 핵심 설정 예상 향상 대용량 순차 I/O splice + max_pages=256 + max_write=1M 처리량 2~3배 소규모 랜덤 I/O passthrough + writeback + clone_fd IOPS 5~10배 메타데이터 집약 READDIRPLUS + parallel_dirops + 긴 entry_timeout ops/s 3~5배 VM 파일 공유 virtiofs + DAX + 멀티 큐 네이티브 대비 85~95% 클라우드 스토리지 writeback + max_background=128 + 긴 attr_timeout 네트워크 지연 마스킹
FUSE 성능 병목: 컨텍스트 스위칭(최대 병목) → 데이터 복사 → 직렬 처리 순으로 최적화

튜닝 파라미터 상세

파라미터기본값권장 범위효과주의사항
max_read128KB128KB~1MB대용량 순차 읽기 처리량(Throughput) 향상데몬 메모리 사용량 증가
max_write128KB128KB~1MB대용량 순차 쓰기 처리량 향상데몬 메모리 사용량 증가
max_pages3232~256단일 요청 데이터 크기 증가커널 메모리 사용량 증가
max_background1212~256동시 배경 요청 수 증가데몬 부하 증가 가능
congestion_threshold9max_background의 75%BDI 혼잡 시작점 조절너무 높으면 메모리 압박
attr_timeout1.0초1~3600초getattr 호출 빈도 감소속성 변경 지연 반영
entry_timeout1.0초1~3600초lookup 호출 빈도 감소이름 변경 지연 반영
negative_timeout0초0~60초존재하지 않는 파일 캐시새 파일 생성 시 지연 감지
max_idle_threads10CPU 코어 수멀티스레드 처리 능력유휴 스레드 메모리 소비

성능 측정 방법

# 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 오버헤드

최적화 적용 체크리스트

단계적 최적화:
  1. 캐시 타임아웃 조정 (가장 간단) — attr_timeout, entry_timeout을 워크로드에 맞게 증가
  2. writeback 캐시 활성화 — 쓰기 워크로드 성능 3~5배 향상
  3. splice 활성화write_buf 콜백 구현으로 대용량 I/O 최적화
  4. 멀티스레드clone_fd=1, 적절한 max_idle_threads 설정
  5. max_background 증가 — 높은 병렬성 워크로드에서 배경 요청 제한 완화
  6. READDIRPLUS 활성화ls -l 패턴의 메타데이터 성능 향상
  7. 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를 사용합니다.

High-level vs Low-level API 비교 High-level API fuse_operations (path 기반) getattr(path), read(path, buf, ...) libfuse 내부 path→inode 변환 계층 fuse_session (커널 통신) Low-level API fuse_lowlevel_ops (inode 기반) lookup(parent, name), read(ino, ...) 경로 변환 없음 (직접 inode 관리) fuse_session (커널 통신) /dev/fuse — FUSE 커널 모듈 (fuse.ko) 단순, 빠른 개발 내부 path 캐시 사용 고성능, 세밀한 제어 nlookup/forget 직접 관리 sshfs, s3fs 등 단순 매핑 glusterfs, ceph-fuse, virtiofsd
High-level API는 libfuse 내부에서 path→inode 변환을 처리하고, Low-level API는 데몬이 직접 inode을 관리합니다

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 누락 → 커널 프로세스 영구 블록 */
nlookup 카운터 관리: 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 메커니즘: 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, &params);
    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 Notify: 데몬 → 커널 방향 알림 FUSE 데몬 원격 변경 감지 (inotify, lease, 폴링 등) Notify API 호출 inval_inode(ino, off, len) inval_entry(parent, name) store(ino, off, data) retrieve(ino, off, size) delete(parent, child, name) /dev/fuse write() with FUSE_NOTIFY_* opcode FUSE 커널 모듈 notify Page Cache 무효화 Dentry Cache 무효화 Page Cache에 데이터 저장 Page Cache에서 데이터 조회 inval_inode: 페이지 캐시의 특정 범위 무효화 inval_entry: dentry 캐시에서 이름 제거 store: 데몬이 커널 캐시를 직접 갱신 retrieve: 커널 캐시 데이터를 데몬이 조회 일반 요청(→): 커널→데몬 | 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);
}
Notify 지원 조건: Notify API는 Low-level API에서만 사용 가능합니다. High-level API(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: 데이터 I/O 데몬 우회 Application (read/write) VFS Layer FUSE 커널 모듈 (fuse.ko) passthrough 판단: backing_fd 설정 여부 일반 경로: /dev/fuse 데몬 → 유저 복사 → 응답 metadata Passthrough: 직접 I/O 커널이 backing fd로 직접 data I/O Backing FS (ext4, xfs...) FUSE Daemon (open만 처리)
Passthrough 모드에서 read/write는 FUSE 데몬을 완전히 우회하고, open/lookup 등 메타데이터만 데몬이 처리합니다

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 가능)
Passthrough 제약사항: passthrough fd가 설정되면 해당 파일의 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캐싱 정책autoalways (읽기 워크로드)
--sandbox격리 모드namespacechroot (성능 우선)
--thread-pool-size워커 스레드 수CPU 수vCPU 수 이상
--inode-file-handlesfile handle 사용nevermandatory (rename 안정성)
--announce-submounts서브마운트 공유offon (여러 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 동작 원리: auto_cache 모드에서는 open() 시 커널이 파일의 mtime/size를 이전 캐시된 값과 비교합니다. 변경이 감지되면 페이지 캐시를 자동으로 무효화합니다. 단일 클라이언트 환경에서 가장 좋은 일관성/성능 균형을 제공합니다.

벤치마크와 성능 튜닝

FUSE 성능 최적화의 핵심은 병목 지점을 정확히 식별하는 것입니다. 컨텍스트 스위치, 메모리 복사, 직렬화(Serialization) 오버헤드 중 어느 것이 지배적인지에 따라 최적화 전략이 달라집니다.

FUSE 성능 병목 3대 요소와 해결 전략 Context Switch 커널↔유저 전환 ~2-5us 요청당 최소 2회 전환 메타데이터 연산에서 지배적 Memory Copy 커널→유저→커널 복사 대용량 I/O에서 지배적 메모리 대역폭 소모 Serialization 단일 /dev/fuse fd 경합 멀티코어 병렬성 제한 높은 동시성에서 지배적 해결: 요청 횟수 감소 - 캐시 timeout 증가 - READDIRPLUS 활성화 해결: 복사 제거 - splice / write_buf - passthrough (6.9+) 해결: 병렬화 - clone_fd=1 (멀티스레드) - max_background 증가 워크로드별 지배적 병목 메타데이터: Context Switch 순차 I/O: Memory Copy 랜덤 I/O: Serialization Passthrough: 전부 해결
워크로드 패턴에 따라 지배적 병목이 달라지며, passthrough는 3가지 모두 우회합니다

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%    │
# └──────────────────┴──────────┴───────────┘
최적화 우선순위(Priority): 대부분의 FUSE 파일시스템에서 가장 효과적인 최적화 순서는 (1) 캐시 타임아웃 증가 → (2) writeback cache → (3) splice/write_buf → (4) 멀티스레드 + clone_fd → (5) passthrough (가능한 경우)입니다. passthrough가 가능하면 나머지 최적화 대부분이 불필요해집니다.

참고 자료

FUSE와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.