FUSE (Filesystem in Userspace)

FUSE는 파일시스템 로직을 커널 밖 사용자 공간에서 구현하게 해주는 프레임워크입니다. 이 문서에서는 `/dev/fuse` 요청/응답 프로토콜, fuse.ko와 libfuse의 역할 분리, lookup/read/write/flush 연산 경로, page cache와 writeback 캐시 정책, 권한 위임과 uid/gid 매핑, virtiofs 기반 가상화 시나리오, 컨텍스트 스위치 오버헤드와 병목 최적화 기법을 상세히 다룹니다.

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

핵심 요약

  • 계층 이해 — VFS, 캐시, 하위 FS 경계를 구분합니다.
  • 메타데이터 우선 — inode/dentry 일관성을 먼저 확인합니다.
  • 저장 정책 — 저널링/압축/할당 정책 차이를 비교합니다.
  • 일관성 모델 — 로컬/원격/합성 FS의 반영 시점을 구분합니다.
  • 복구 관점 — 장애 시 재구성 경로를 함께 점검합니다.

단계별 이해

  1. 경계 계층 파악
    요청이 VFS에서 어디로 내려가는지 확인합니다.
  2. 메타/데이터 분리
    어느 경로에서 무엇이 갱신되는지 나눠 봅니다.
  3. 동기화/플러시 확인
    쓰기 반영 시점과 순서를 검증합니다.
  4. 복구 시나리오 점검
    비정상 종료 후 일관성 회복을 확인합니다.
관련 표준: FUSE Protocol (커널 ↔ 유저스페이스 메시지 프로토콜), POSIX.1-2017 (파일 시맨틱) — FUSE는 사용자 공간에서 POSIX 호환 파일시스템을 구현할 수 있게 합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

FUSE 개요

FUSE(Filesystem in Userspace)는 커널 모듈을 직접 작성하지 않고도 유저스페이스 프로그램으로 파일시스템을 구현할 수 있게 해주는 프레임워크입니다. 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() 등 시스템 콜 호출
  2. VFS가 FUSE 파일시스템의 file_operations/inode_operations을 통해 FUSE 커널 모듈로 디스패치
  3. FUSE 커널 모듈이 fuse_req 구조체를 생성하여 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 참조 카운트 감소 (응답 없음)
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: 커널 → 유저 비동기 알림

커널은 유저 데몬에 비동기 알림을 보낼 수 있습니다. 이는 캐시 무효화 등에 사용됩니다:

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_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

성능 특성과 최적화

유저-커널 컨텍스트 스위칭 오버헤드

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를 활성화하면 커널이 쓰기 데이터를 페이지 캐시에 버퍼링하여 배치 처리합니다. 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

컨테이너 환경 활용

Kata Containers는 virtiofs를 사용하여 컨테이너 이미지와 볼륨을 경량 VM 내부에 공유합니다. 이를 통해 컨테이너 수준의 편의성과 VM 수준의 격리를 동시에 달성합니다.

보안 모델

non-root 마운트와 보안 고려사항

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

default_permissions vs 커스텀 접근 제어

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

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

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

/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 원자적
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()를 호출합니다. 멀티스레드 처리 시 여러 스레드가 동일 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 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 경로(페이지 참조만 이동)

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 콜백과 일반 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)빠름 (캐시 히트)
크래시 시 데이터안전 (이미 전달)유실 가능 (더티 페이지)
다중 접근 일관성강한 일관성약한 일관성 (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을 사용할 때는 반드시 분산 잠금 메커니즘을 함께 구현해야 합니다.

DAX (Direct Access) 모드

FUSE DAX는 호스트 메모리의 파일 데이터를 게스트 VM의 주소 공간에 직접 매핑하는 기술입니다. 데이터 복사 없이 메모리 매핑된 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 소켓 → 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 요청 파이프라인에서 추적 가능한 지점을 보여줍니다.

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대용량 순차 읽기 처리량 향상데몬 메모리 사용량 증가
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와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.